Initial QSfera import
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
coverage.out
|
||||
coverage.html
|
||||
vendor/
|
||||
|
||||
# IDE-specific settings
|
||||
.idea
|
||||
.vscode
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Configuration file for golangci-lint
|
||||
#
|
||||
# https://github.com/golangci/golangci-lint
|
||||
#
|
||||
# fighting with false positives?
|
||||
# https://github.com/golangci/golangci-lint#nolint
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false]
|
||||
- errcheck # Inspects source code for security problems [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]
|
||||
- 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]
|
||||
- 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]
|
||||
- misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true]
|
||||
- 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]
|
||||
- revive # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [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]
|
||||
- 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]
|
||||
- unconvert # Remove unnecessary type conversions [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]
|
||||
|
||||
disable:
|
||||
# TODO(ross): fix errors reported by these checkers and enable them
|
||||
- dupl # Tool for code clone detection [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]
|
||||
- lll # Reports long lines [fast: true, auto-fix: false]
|
||||
- depguard # Go linter that checks if package imports are in a list of acceptable packages [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)
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
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.
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
# 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(), "displayName"))
|
||||
}
|
||||
|
||||
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.
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
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
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const flateUncompressLimit = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
func newSaferFlateReader(r io.Reader) io.ReadCloser {
|
||||
return &saferFlateReader{r: flate.NewReader(r)}
|
||||
}
|
||||
|
||||
type saferFlateReader struct {
|
||||
r io.ReadCloser
|
||||
count int
|
||||
}
|
||||
|
||||
func (r *saferFlateReader) Read(p []byte) (n int, err error) {
|
||||
if r.count+len(p) > flateUncompressLimit {
|
||||
return 0, fmt.Errorf("flate: uncompress limit exceeded (%d bytes)", flateUncompressLimit)
|
||||
}
|
||||
n, err = r.r.Read(p)
|
||||
r.count += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *saferFlateReader) Close() error {
|
||||
return r.r.Close()
|
||||
}
|
||||
+1095
File diff suppressed because it is too large
Load Diff
+32
@@ -0,0 +1,32 @@
|
||||
// Package logger provides a logging interface.
|
||||
package logger
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Interface provides the minimal logging interface
|
||||
type Interface interface {
|
||||
// Printf prints to the logger using the format.
|
||||
Printf(format string, v ...interface{})
|
||||
// Print prints to the logger.
|
||||
Print(v ...interface{})
|
||||
// Println prints new line.
|
||||
Println(v ...interface{})
|
||||
// Fatal is equivalent to Print() followed by a call to os.Exit(1).
|
||||
Fatal(v ...interface{})
|
||||
// Fatalf is equivalent to Printf() followed by a call to os.Exit(1).
|
||||
Fatalf(format string, v ...interface{})
|
||||
// Fatalln is equivalent to Println() followed by a call to os.Exit(1).
|
||||
Fatalln(v ...interface{})
|
||||
// Panic is equivalent to Print() followed by a call to panic().
|
||||
Panic(v ...interface{})
|
||||
// Panicf is equivalent to Printf() followed by a call to panic().
|
||||
Panicf(format string, v ...interface{})
|
||||
// Panicln is equivalent to Println() followed by a call to panic().
|
||||
Panicln(v ...interface{})
|
||||
}
|
||||
|
||||
// DefaultLogger logs messages to os.Stdout
|
||||
var DefaultLogger = log.New(os.Stdout, "", log.LstdFlags)
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
// HTTPPostBinding is the official URN for the HTTP-POST binding (transport)
|
||||
const HTTPPostBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||
|
||||
// HTTPRedirectBinding is the official URN for the HTTP-Redirect binding (transport)
|
||||
const HTTPRedirectBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
|
||||
// HTTPArtifactBinding is the official URN for the HTTP-Artifact binding (transport)
|
||||
const HTTPArtifactBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
|
||||
|
||||
// SOAPBinding is the official URN for the SOAP binding (transport)
|
||||
const SOAPBinding = "urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
|
||||
|
||||
// SOAPBindingV1 is the URN for the SOAP binding in SAML 1.0
|
||||
const SOAPBindingV1 = "urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
|
||||
|
||||
// EntitiesDescriptor represents the SAML object of the same name.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.3.1
|
||||
type EntitiesDescriptor struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntitiesDescriptor"`
|
||||
ID *string `xml:",attr,omitempty"`
|
||||
ValidUntil *time.Time `xml:"validUntil,attr,omitempty"`
|
||||
CacheDuration *time.Duration `xml:"cacheDuration,attr,omitempty"`
|
||||
Name *string `xml:",attr,omitempty"`
|
||||
Signature *etree.Element
|
||||
EntitiesDescriptors []EntitiesDescriptor `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntitiesDescriptor"`
|
||||
EntityDescriptors []EntityDescriptor `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"`
|
||||
}
|
||||
|
||||
// Metadata as been renamed to EntityDescriptor
|
||||
//
|
||||
// This change was made to be consistent with the rest of the API which uses names
|
||||
// from the SAML specification for types.
|
||||
//
|
||||
// This is a tombstone to help you discover this fact. You should update references
|
||||
// to saml.Metadata to be saml.EntityDescriptor.
|
||||
var Metadata = struct{}{}
|
||||
|
||||
// EntityDescriptor represents the SAML EntityDescriptor object.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.3.2
|
||||
type EntityDescriptor struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"`
|
||||
EntityID string `xml:"entityID,attr"`
|
||||
ID string `xml:",attr,omitempty"`
|
||||
ValidUntil time.Time `xml:"validUntil,attr,omitempty"`
|
||||
CacheDuration time.Duration `xml:"cacheDuration,attr,omitempty"`
|
||||
Signature *etree.Element
|
||||
RoleDescriptors []RoleDescriptor `xml:"RoleDescriptor"`
|
||||
IDPSSODescriptors []IDPSSODescriptor `xml:"IDPSSODescriptor"`
|
||||
SPSSODescriptors []SPSSODescriptor `xml:"SPSSODescriptor"`
|
||||
AuthnAuthorityDescriptors []AuthnAuthorityDescriptor `xml:"AuthnAuthorityDescriptor"`
|
||||
AttributeAuthorityDescriptors []AttributeAuthorityDescriptor `xml:"AttributeAuthorityDescriptor"`
|
||||
PDPDescriptors []PDPDescriptor `xml:"PDPDescriptor"`
|
||||
AffiliationDescriptor *AffiliationDescriptor
|
||||
Organization *Organization
|
||||
ContactPerson *ContactPerson
|
||||
AdditionalMetadataLocations []string `xml:"AdditionalMetadataLocation"`
|
||||
}
|
||||
|
||||
// MarshalXML implements xml.Marshaler
|
||||
func (m EntityDescriptor) MarshalXML(e *xml.Encoder, _ xml.StartElement) error {
|
||||
type Alias EntityDescriptor
|
||||
aux := &struct {
|
||||
ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
|
||||
CacheDuration Duration `xml:"cacheDuration,attr,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
ValidUntil: RelaxedTime(m.ValidUntil),
|
||||
CacheDuration: Duration(m.CacheDuration),
|
||||
Alias: (*Alias)(&m),
|
||||
}
|
||||
return e.Encode(aux)
|
||||
}
|
||||
|
||||
// UnmarshalXML implements xml.Unmarshaler
|
||||
func (m *EntityDescriptor) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type Alias EntityDescriptor
|
||||
aux := &struct {
|
||||
ValidUntil RelaxedTime `xml:"validUntil,attr,omitempty"`
|
||||
CacheDuration Duration `xml:"cacheDuration,attr,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(m),
|
||||
}
|
||||
if err := d.DecodeElement(aux, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
m.ValidUntil = time.Time(aux.ValidUntil)
|
||||
m.CacheDuration = time.Duration(aux.CacheDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Organization represents the SAML Organization object.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.3.2.1
|
||||
type Organization struct {
|
||||
OrganizationNames []LocalizedName `xml:"OrganizationName"`
|
||||
OrganizationDisplayNames []LocalizedName `xml:"OrganizationDisplayName"`
|
||||
OrganizationURLs []LocalizedURI `xml:"OrganizationURL"`
|
||||
}
|
||||
|
||||
// LocalizedName represents the SAML type localizedNameType.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.2.4
|
||||
type LocalizedName struct {
|
||||
Lang string `xml:"http://www.w3.org/XML/1998/namespace lang,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// LocalizedURI represents the SAML type localizedURIType.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.2.5
|
||||
type LocalizedURI struct {
|
||||
Lang string `xml:"http://www.w3.org/XML/1998/namespace lang,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// ContactPerson represents the SAML element ContactPerson.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.3.2.2
|
||||
type ContactPerson struct {
|
||||
ContactType string `xml:"contactType,attr"`
|
||||
Company string
|
||||
GivenName string
|
||||
SurName string
|
||||
EmailAddresses []string `xml:"EmailAddress"`
|
||||
TelephoneNumbers []string `xml:"TelephoneNumber"`
|
||||
}
|
||||
|
||||
// RoleDescriptor represents the SAML element RoleDescriptor.
|
||||
//
|
||||
// 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"`
|
||||
CacheDuration time.Duration `xml:"cacheDuration,attr,omitempty"`
|
||||
ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"`
|
||||
ErrorURL string `xml:"errorURL,attr,omitempty"`
|
||||
Signature *etree.Element
|
||||
KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor,omitempty"`
|
||||
Organization *Organization `xml:"Organization,omitempty"`
|
||||
ContactPeople []ContactPerson `xml:"ContactPerson,omitempty"`
|
||||
}
|
||||
|
||||
// KeyDescriptor represents the XMLSEC object of the same name
|
||||
type KeyDescriptor struct {
|
||||
Use string `xml:"use,attr"`
|
||||
KeyInfo KeyInfo `xml:"http://www.w3.org/2000/09/xmldsig# KeyInfo"`
|
||||
EncryptionMethods []EncryptionMethod `xml:"EncryptionMethod"`
|
||||
}
|
||||
|
||||
// EncryptionMethod represents the XMLSEC object of the same name
|
||||
type EncryptionMethod struct {
|
||||
Algorithm string `xml:"Algorithm,attr"`
|
||||
}
|
||||
|
||||
// KeyInfo represents the XMLSEC object of the same name
|
||||
type KeyInfo struct {
|
||||
XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# KeyInfo"`
|
||||
X509Data X509Data `xml:"X509Data"`
|
||||
}
|
||||
|
||||
// X509Data represents the XMLSEC object of the same name
|
||||
type X509Data struct {
|
||||
XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# X509Data"`
|
||||
X509Certificates []X509Certificate `xml:"X509Certificate"`
|
||||
}
|
||||
|
||||
// X509Certificate represents the XMLSEC object of the same name
|
||||
type X509Certificate struct {
|
||||
XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# X509Certificate"`
|
||||
Data string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// Endpoint represents the SAML EndpointType object.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.2.2
|
||||
type Endpoint struct {
|
||||
Binding string `xml:"Binding,attr"`
|
||||
Location string `xml:"Location,attr"`
|
||||
ResponseLocation string `xml:"ResponseLocation,attr,omitempty"`
|
||||
}
|
||||
|
||||
func checkEndpointLocation(binding string, location string) (string, error) {
|
||||
// Within the SAML standard, the complex type EndpointType describes a
|
||||
// SAML protocol binding endpoint at which a SAML entity can be sent
|
||||
// protocol messages. In particular, the location of an endpoint type is
|
||||
// defined as follows in the Metadata for the OASIS Security Assertion
|
||||
// Markup Language (SAML) V2.0 - 2.2.2 Complex Type EndpointType:
|
||||
//
|
||||
// Location [Required] A required URI attribute that specifies the
|
||||
// location of the endpoint. The allowable syntax of this URI depends
|
||||
// on the protocol binding.
|
||||
switch binding {
|
||||
case HTTPPostBinding,
|
||||
HTTPRedirectBinding,
|
||||
HTTPArtifactBinding,
|
||||
SOAPBinding,
|
||||
SOAPBindingV1:
|
||||
locationURL, err := url.Parse(location)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid url %q: %w", location, err)
|
||||
}
|
||||
switch locationURL.Scheme {
|
||||
case "http", "https":
|
||||
// ok
|
||||
default:
|
||||
return "", fmt.Errorf("invalid url scheme %q for binding %q",
|
||||
locationURL.Scheme, binding)
|
||||
}
|
||||
default:
|
||||
// We don't know what form location should take, but the protocol
|
||||
// requires that we validate its syntax.
|
||||
//
|
||||
// In practice, lots of metadata contains random bindings, for example
|
||||
// "urn:mace:shibboleth:1.0:profiles:AuthnRequest" from our own test suite.
|
||||
//
|
||||
// We can't fail, but we also can't allow a location parameter whose syntax we
|
||||
// cannot verify. The least-bad course of action here is to set location to
|
||||
// and empty string, and hope the caller doesn't care need it.
|
||||
location = ""
|
||||
}
|
||||
|
||||
return location, nil
|
||||
}
|
||||
|
||||
// UnmarshalXML implements xml.Unmarshaler
|
||||
func (m *Endpoint) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type Alias Endpoint
|
||||
aux := &struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(m),
|
||||
}
|
||||
if err := d.DecodeElement(aux, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
m.Location, err = checkEndpointLocation(m.Binding, m.Location)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m.ResponseLocation != "" {
|
||||
m.ResponseLocation, err = checkEndpointLocation(m.Binding, m.ResponseLocation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IndexedEndpoint represents the SAML IndexedEndpointType object.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.2.3
|
||||
type IndexedEndpoint struct {
|
||||
Binding string `xml:"Binding,attr"`
|
||||
Location string `xml:"Location,attr"`
|
||||
ResponseLocation *string `xml:"ResponseLocation,attr,omitempty"`
|
||||
Index int `xml:"index,attr"`
|
||||
IsDefault *bool `xml:"isDefault,attr"`
|
||||
}
|
||||
|
||||
// UnmarshalXML implements xml.Unmarshaler
|
||||
func (m *IndexedEndpoint) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type Alias IndexedEndpoint
|
||||
aux := &struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(m),
|
||||
}
|
||||
if err := d.DecodeElement(aux, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
m.Location, err = checkEndpointLocation(m.Binding, m.Location)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m.ResponseLocation != nil {
|
||||
responseLocation, err := checkEndpointLocation(m.Binding, *m.ResponseLocation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if responseLocation != "" {
|
||||
m.ResponseLocation = &responseLocation
|
||||
} else {
|
||||
m.ResponseLocation = nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SSODescriptor represents the SAML complex type SSODescriptor
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.2
|
||||
type SSODescriptor struct {
|
||||
RoleDescriptor
|
||||
ArtifactResolutionServices []IndexedEndpoint `xml:"ArtifactResolutionService"`
|
||||
SingleLogoutServices []Endpoint `xml:"SingleLogoutService"`
|
||||
ManageNameIDServices []Endpoint `xml:"ManageNameIDService"`
|
||||
NameIDFormats []NameIDFormat `xml:"NameIDFormat"`
|
||||
}
|
||||
|
||||
// IDPSSODescriptor represents the SAML IDPSSODescriptorType object.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.3
|
||||
type IDPSSODescriptor struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"`
|
||||
SSODescriptor
|
||||
WantAuthnRequestsSigned *bool `xml:",attr"`
|
||||
|
||||
SingleSignOnServices []Endpoint `xml:"SingleSignOnService"`
|
||||
ArtifactResolutionServices []Endpoint `xml:"ArtifactResolutionService"`
|
||||
NameIDMappingServices []Endpoint `xml:"NameIDMappingService"`
|
||||
AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"`
|
||||
AttributeProfiles []string `xml:"AttributeProfile"`
|
||||
Attributes []Attribute `xml:"Attribute"`
|
||||
}
|
||||
|
||||
// SPSSODescriptor represents the SAML SPSSODescriptorType object.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.2
|
||||
type SPSSODescriptor struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata SPSSODescriptor"`
|
||||
SSODescriptor
|
||||
AuthnRequestsSigned *bool `xml:",attr"`
|
||||
WantAssertionsSigned *bool `xml:",attr"`
|
||||
AssertionConsumerServices []IndexedEndpoint `xml:"AssertionConsumerService"`
|
||||
AttributeConsumingServices []AttributeConsumingService `xml:"AttributeConsumingService"`
|
||||
}
|
||||
|
||||
// AttributeConsumingService represents the SAML AttributeConsumingService object.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.4.1
|
||||
type AttributeConsumingService struct {
|
||||
Index int `xml:"index,attr"`
|
||||
IsDefault *bool `xml:"isDefault,attr"`
|
||||
ServiceNames []LocalizedName `xml:"ServiceName"`
|
||||
ServiceDescriptions []LocalizedName `xml:"ServiceDescription"`
|
||||
RequestedAttributes []RequestedAttribute `xml:"RequestedAttribute"`
|
||||
}
|
||||
|
||||
// RequestedAttribute represents the SAML RequestedAttribute object.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.4.2
|
||||
type RequestedAttribute struct {
|
||||
Attribute
|
||||
IsRequired *bool `xml:"isRequired,attr"`
|
||||
}
|
||||
|
||||
// AuthnAuthorityDescriptor represents the SAML AuthnAuthorityDescriptor object.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.5
|
||||
type AuthnAuthorityDescriptor struct {
|
||||
RoleDescriptor
|
||||
AuthnQueryServices []Endpoint `xml:"AuthnQueryService"`
|
||||
AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"`
|
||||
NameIDFormats []NameIDFormat `xml:"NameIDFormat"`
|
||||
}
|
||||
|
||||
// PDPDescriptor represents the SAML PDPDescriptor object.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.6
|
||||
type PDPDescriptor struct {
|
||||
RoleDescriptor
|
||||
AuthzServices []Endpoint `xml:"AuthzService"`
|
||||
AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"`
|
||||
NameIDFormats []NameIDFormat `xml:"NameIDFormat"`
|
||||
}
|
||||
|
||||
// AttributeAuthorityDescriptor represents the SAML AttributeAuthorityDescriptor object.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.7
|
||||
type AttributeAuthorityDescriptor struct {
|
||||
RoleDescriptor
|
||||
AttributeServices []Endpoint `xml:"AttributeService"`
|
||||
AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"`
|
||||
NameIDFormats []NameIDFormat `xml:"NameIDFormat"`
|
||||
AttributeProfiles []string `xml:"AttributeProfile"`
|
||||
Attributes []Attribute `xml:"Attribute"`
|
||||
}
|
||||
|
||||
// AffiliationDescriptor represents the SAML AffiliationDescriptor object.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.5
|
||||
type AffiliationDescriptor struct {
|
||||
AffiliationOwnerID string `xml:"affiliationOwnerID,attr"`
|
||||
ID string `xml:",attr"`
|
||||
ValidUntil time.Time `xml:"validUntil,attr,omitempty"`
|
||||
CacheDuration time.Duration `xml:"cacheDuration,attr"`
|
||||
Signature *etree.Element
|
||||
AffiliateMembers []string `xml:"AffiliateMember"`
|
||||
KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor"`
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
// 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
|
||||
//
|
||||
// Version 0.4.0 introduces a few breaking changes to the _samlsp_ package in order to make the package more extensible, and to clean up the interfaces a bit. The default behavior remains the same, but you can now provide interface implementations of _RequestTracker_ (which tracks pending requests), _Session_ (which handles maintaining a session) and _OnError_ which handles reporting errors.
|
||||
//
|
||||
// Public fields of _samlsp.Middleware_ have changed, so some usages may require adjustment. See [issue 231](https://github.com/crewjam/saml/issues/231) for details.
|
||||
//
|
||||
// The option to provide an IDP metadata URL has been deprecated. Instead, we recommend that you use the `FetchMetadata()` function, or fetch the metadata yourself and use the new `ParseMetadata()` function, and pass the metadata in _samlsp.Options.IDPMetadata_.
|
||||
//
|
||||
// Similarly, the _HTTPClient_ field is now deprecated because it was only used for fetching metdata, which is no longer directly implemented.
|
||||
//
|
||||
// The fields that manage how cookies are set are deprecated as well. To customize how cookies are managed, provide custom implementation of _RequestTracker_ and/or _Session_, perhaps by extending the default implementations.
|
||||
//
|
||||
// The deprecated fields have not been removed from the Options structure, but will be in future.
|
||||
//
|
||||
// In particular we have deprecated the following fields in
|
||||
// _samlsp.Options_:
|
||||
//
|
||||
// - `Logger` - This was used to emit errors while validating, which is an anti-pattern.
|
||||
// - `IDPMetadataURL` - Instead use `FetchMetadata()`
|
||||
// - `HTTPClient` - Instead pass httpClient to FetchMetadata
|
||||
// - `CookieMaxAge` - Instead assign a custom CookieRequestTracker or CookieSessionProvider
|
||||
// - `CookieName` - Instead assign a custom CookieRequestTracker or CookieSessionProvider
|
||||
// - `CookieDomain` - Instead assign a custom CookieRequestTracker or CookieSessionProvider
|
||||
// - `CookieDomain` - Instead assign a custom CookieRequestTracker or CookieSessionProvider
|
||||
//
|
||||
// # 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 (
|
||||
//
|
||||
// "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.Token(r.Context()).Attributes.Get("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
|
||||
// }
|
||||
//
|
||||
// rootURL, err := url.Parse("http://localhost:8000")
|
||||
// if err != nil {
|
||||
// panic(err) // TODO handle error
|
||||
// }
|
||||
//
|
||||
// samlSP, _ := samlsp.New(samlsp.Options{
|
||||
// URL: *rootURL,
|
||||
// Key: keyPair.PrivateKey.(*rsa.PrivateKey),
|
||||
// Certificate: keyPair.Leaf,
|
||||
// IDPMetadataURL: idpMetadataURL,
|
||||
// })
|
||||
// app := http.HandlerFunc(hello)
|
||||
// http.Handle("/hello", samlSP.RequireAccount(app))
|
||||
// http.Handle("/saml/", samlSP)
|
||||
// http.ListenAndServe(":8000", nil)
|
||||
// }
|
||||
//
|
||||
// ```
|
||||
//
|
||||
// Next we'll have to register our service provider with the 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](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 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)).
|
||||
package saml
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package saml
|
||||
|
||||
//go:generate bash -c "(cat README.md | grep -E -v '^# SAML' | sed 's|^## ||g' | sed 's|\\*\\*||g' | sed 's|^|// |g'; echo 'package saml') > saml.go"
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package samlsp
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
)
|
||||
|
||||
// ErrorFunction is a callback that is invoked to return an error to the
|
||||
// web user.
|
||||
type ErrorFunction func(w http.ResponseWriter, r *http.Request, err error)
|
||||
|
||||
// DefaultOnError is the default ErrorFunction implementation. It prints
|
||||
// an message via the standard log package and returns a simple text
|
||||
// "Forbidden" message to the user.
|
||||
func DefaultOnError(w http.ResponseWriter, _ *http.Request, err error) {
|
||||
if parseErr, ok := err.(*saml.InvalidResponseError); ok {
|
||||
log.Printf("WARNING: received invalid saml response: %s (now: %s) %s",
|
||||
parseErr.Response, parseErr.Now, parseErr.PrivateErr)
|
||||
} else {
|
||||
log.Printf("ERROR: %s", err)
|
||||
}
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package samlsp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/crewjam/httperr"
|
||||
xrv "github.com/mattermost/xml-roundtrip-validator"
|
||||
|
||||
"github.com/crewjam/saml/logger"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
)
|
||||
|
||||
// ParseMetadata parses arbitrary SAML IDP metadata.
|
||||
//
|
||||
// Note: this is needed because IDP metadata is sometimes wrapped in
|
||||
// an <EntitiesDescriptor>, and sometimes the top level element is an
|
||||
// <EntityDescriptor>.
|
||||
func ParseMetadata(data []byte) (*saml.EntityDescriptor, error) {
|
||||
entity := &saml.EntityDescriptor{}
|
||||
|
||||
if err := xrv.Validate(bytes.NewBuffer(data)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err := xml.Unmarshal(data, entity)
|
||||
|
||||
// this comparison is ugly, but it is how the error is generated in encoding/xml
|
||||
if err != nil && err.Error() == "expected element type <EntityDescriptor> but have <EntitiesDescriptor>" {
|
||||
entities := &saml.EntitiesDescriptor{}
|
||||
if err := xml.Unmarshal(data, entities); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, e := range entities.EntityDescriptors {
|
||||
if len(e.IDPSSODescriptors) > 0 {
|
||||
return &entities.EntityDescriptors[i], nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("no entity found with IDPSSODescriptor")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entity, nil
|
||||
}
|
||||
|
||||
// FetchMetadata returns metadata from an IDP metadata URL.
|
||||
func FetchMetadata(ctx context.Context, httpClient *http.Client, metadataURL url.URL) (*saml.EntityDescriptor, error) {
|
||||
req, err := http.NewRequest("GET", metadataURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logger.DefaultLogger.Printf("Error while closing response body during fetch metadata: %v", err)
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, httperr.Response(*resp)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ParseMetadata(data)
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
package samlsp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
)
|
||||
|
||||
// Middleware implements middleware than allows a web application
|
||||
// to support SAML.
|
||||
//
|
||||
// It implements http.Handler so that it can provide the metadata and ACS endpoints,
|
||||
// typically /saml/metadata and /saml/acs, respectively.
|
||||
//
|
||||
// It also provides middleware RequireAccount which redirects users to
|
||||
// the auth process if they do not have session credentials.
|
||||
//
|
||||
// When redirecting the user through the SAML auth flow, the middleware assigns
|
||||
// a temporary cookie with a random name beginning with "saml_". The value of
|
||||
// the cookie is a signed JSON Web Token containing the original URL requested
|
||||
// and the SAML request ID. The random part of the name corresponds to the
|
||||
// RelayState parameter passed through the SAML flow.
|
||||
//
|
||||
// When validating the SAML response, the RelayState is used to look up the
|
||||
// correct cookie, validate that the SAML request ID, and redirect the user
|
||||
// back to their original URL.
|
||||
//
|
||||
// Sessions are established by issuing a JSON Web Token (JWT) as a session
|
||||
// cookie once the SAML flow has succeeded. The JWT token contains the
|
||||
// authenticated attributes from the SAML assertion.
|
||||
//
|
||||
// When the middleware receives a request with a valid session JWT it extracts
|
||||
// the SAML attributes and modifies the http.Request object adding a Context
|
||||
// object to the request context that contains attributes from the initial
|
||||
// SAML assertion.
|
||||
//
|
||||
// When issuing JSON Web Tokens, a signing key is required. Because the
|
||||
// SAML service provider already has a private key, we borrow that key
|
||||
// to sign the JWTs as well.
|
||||
type Middleware struct {
|
||||
ServiceProvider saml.ServiceProvider
|
||||
OnError func(w http.ResponseWriter, r *http.Request, err error)
|
||||
Binding string // either saml.HTTPPostBinding or saml.HTTPRedirectBinding
|
||||
ResponseBinding string // either saml.HTTPPostBinding or saml.HTTPArtifactBinding
|
||||
RequestTracker RequestTracker
|
||||
Session SessionProvider
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler and serves the SAML-specific HTTP endpoints
|
||||
// on the URIs specified by m.ServiceProvider.MetadataURL and
|
||||
// m.ServiceProvider.AcsURL.
|
||||
func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == m.ServiceProvider.MetadataURL.Path {
|
||||
m.ServeMetadata(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Path == m.ServiceProvider.AcsURL.Path {
|
||||
m.ServeACS(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.NotFoundHandler().ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// ServeMetadata handles requests for the SAML metadata endpoint.
|
||||
func (m *Middleware) ServeMetadata(w http.ResponseWriter, _ *http.Request) {
|
||||
buf, _ := xml.MarshalIndent(m.ServiceProvider.Metadata(), "", " ")
|
||||
w.Header().Set("Content-Type", "application/samlmetadata+xml")
|
||||
if _, err := w.Write(buf); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ServeACS handles requests for the SAML ACS endpoint.
|
||||
func (m *Middleware) ServeACS(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
m.OnError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
possibleRequestIDs := []string{}
|
||||
if m.ServiceProvider.AllowIDPInitiated {
|
||||
possibleRequestIDs = append(possibleRequestIDs, "")
|
||||
}
|
||||
|
||||
trackedRequests := m.RequestTracker.GetTrackedRequests(r)
|
||||
for _, tr := range trackedRequests {
|
||||
possibleRequestIDs = append(possibleRequestIDs, tr.SAMLRequestID)
|
||||
}
|
||||
|
||||
assertion, err := m.ServiceProvider.ParseResponse(r, possibleRequestIDs)
|
||||
if err != nil {
|
||||
m.OnError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
m.CreateSessionFromAssertion(w, r, assertion, m.ServiceProvider.DefaultRedirectURI)
|
||||
}
|
||||
|
||||
// RequireAccount is HTTP middleware that requires that each request be
|
||||
// associated with a valid session. If the request is not associated with a valid
|
||||
// session, then rather than serve the request, the middleware redirects the user
|
||||
// to start the SAML auth flow.
|
||||
func (m *Middleware) RequireAccount(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := m.Session.GetSession(r)
|
||||
if session != nil {
|
||||
r = r.WithContext(ContextWithSession(r.Context(), session))
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if err == ErrNoSession {
|
||||
m.HandleStartAuthFlow(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
m.OnError(w, r, err)
|
||||
})
|
||||
}
|
||||
|
||||
// HandleStartAuthFlow is called to start the SAML authentication process.
|
||||
func (m *Middleware) HandleStartAuthFlow(w http.ResponseWriter, r *http.Request) {
|
||||
// If we try to redirect when the original request is the ACS URL we'll
|
||||
// end up in a loop. This is a programming error, so we panic here. In
|
||||
// general this means a 500 to the user, which is preferable to a
|
||||
// redirect loop.
|
||||
if r.URL.Path == m.ServiceProvider.AcsURL.Path {
|
||||
panic("don't wrap Middleware with RequireAccount")
|
||||
}
|
||||
|
||||
var binding, bindingLocation string
|
||||
if m.Binding != "" {
|
||||
binding = m.Binding
|
||||
bindingLocation = m.ServiceProvider.GetSSOBindingLocation(binding)
|
||||
} else {
|
||||
binding = saml.HTTPRedirectBinding
|
||||
bindingLocation = m.ServiceProvider.GetSSOBindingLocation(binding)
|
||||
if bindingLocation == "" {
|
||||
binding = saml.HTTPPostBinding
|
||||
bindingLocation = m.ServiceProvider.GetSSOBindingLocation(binding)
|
||||
}
|
||||
}
|
||||
|
||||
authReq, err := m.ServiceProvider.MakeAuthenticationRequest(bindingLocation, binding, m.ResponseBinding)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// relayState is limited to 80 bytes but also must be integrity protected.
|
||||
// this means that we cannot use a JWT because it is way to long. Instead
|
||||
// we set a signed cookie that encodes the original URL which we'll check
|
||||
// against the SAML response when we get it.
|
||||
relayState, err := m.RequestTracker.TrackRequest(w, r, authReq.ID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if binding == saml.HTTPRedirectBinding {
|
||||
redirectURL, err := authReq.Redirect(relayState, &m.ServiceProvider)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Add("Location", redirectURL.String())
|
||||
w.WriteHeader(http.StatusFound)
|
||||
return
|
||||
}
|
||||
if binding == saml.HTTPPostBinding {
|
||||
w.Header().Add("Content-Security-Policy", ""+
|
||||
"default-src; "+
|
||||
"script-src 'sha256-AjPdJSbZmeWHnEc5ykvJFay8FTWeTeRbs9dutfZ0HqE='; "+
|
||||
"reflected-xss block; referrer no-referrer;")
|
||||
w.Header().Add("Content-type", "text/html")
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(`<!DOCTYPE html><html><body>`)
|
||||
buf.Write(authReq.Post(relayState))
|
||||
buf.WriteString(`</body></html>`)
|
||||
if _, err := w.Write(buf.Bytes()); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
// CreateSessionFromAssertion is invoked by ServeHTTP when we have a new, valid SAML assertion.
|
||||
func (m *Middleware) CreateSessionFromAssertion(w http.ResponseWriter, r *http.Request, assertion *saml.Assertion, redirectURI string) {
|
||||
if trackedRequestIndex := r.Form.Get("RelayState"); trackedRequestIndex != "" {
|
||||
trackedRequest, err := m.RequestTracker.GetTrackedRequest(r, trackedRequestIndex)
|
||||
if err != nil {
|
||||
if err == http.ErrNoCookie && m.ServiceProvider.AllowIDPInitiated {
|
||||
if uri := r.Form.Get("RelayState"); uri != "" {
|
||||
redirectURI = uri
|
||||
}
|
||||
} else {
|
||||
m.OnError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := m.RequestTracker.StopTrackingRequest(w, r, trackedRequestIndex); err != nil {
|
||||
m.OnError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURI = trackedRequest.URI
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.Session.CreateSession(w, r, assertion); err != nil {
|
||||
m.OnError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, redirectURI, http.StatusFound)
|
||||
}
|
||||
|
||||
// RequireAttribute returns a middleware function that requires that the
|
||||
// SAML attribute `name` be set to `value`. This can be used to require
|
||||
// that a remote user be a member of a group. It relies on the Claims assigned
|
||||
// to to the context in RequireAccount.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// goji.Use(m.RequireAccount)
|
||||
// goji.Use(RequireAttributeMiddleware("eduPersonAffiliation", "Staff"))
|
||||
func RequireAttribute(name, value string) func(http.Handler) http.Handler {
|
||||
return func(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if session := SessionFromContext(r.Context()); session != nil {
|
||||
// this will panic if we have the wrong type of Session, and that is OK.
|
||||
sessionWithAttributes := session.(SessionWithAttributes)
|
||||
attributes := sessionWithAttributes.GetAttributes()
|
||||
if values, ok := attributes[name]; ok {
|
||||
for _, v := range values {
|
||||
if v == value {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
// Package samlsp provides helpers that can be used to protect web services using SAML.
|
||||
package samlsp
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
dsig "github.com/russellhaering/goxmldsig"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
)
|
||||
|
||||
// Options represents the parameters for creating a new middleware
|
||||
type Options struct {
|
||||
EntityID string
|
||||
URL url.URL
|
||||
Key *rsa.PrivateKey
|
||||
Certificate *x509.Certificate
|
||||
Intermediates []*x509.Certificate
|
||||
HTTPClient *http.Client
|
||||
AllowIDPInitiated bool
|
||||
DefaultRedirectURI string
|
||||
IDPMetadata *saml.EntityDescriptor
|
||||
SignRequest bool
|
||||
UseArtifactResponse bool
|
||||
ForceAuthn bool // TODO(ross): this should be *bool
|
||||
RequestedAuthnContext *saml.RequestedAuthnContext
|
||||
CookieSameSite http.SameSite
|
||||
CookieName string
|
||||
RelayStateFunc func(w http.ResponseWriter, r *http.Request) string
|
||||
LogoutBindings []string
|
||||
}
|
||||
|
||||
// DefaultSessionCodec returns the default SessionCodec for the provided options,
|
||||
// a JWTSessionCodec configured to issue signed tokens.
|
||||
func DefaultSessionCodec(opts Options) JWTSessionCodec {
|
||||
return JWTSessionCodec{
|
||||
SigningMethod: defaultJWTSigningMethod,
|
||||
Audience: opts.URL.String(),
|
||||
Issuer: opts.URL.String(),
|
||||
MaxAge: defaultSessionMaxAge,
|
||||
Key: opts.Key,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultSessionProvider returns the default SessionProvider for the provided options,
|
||||
// a CookieSessionProvider configured to store sessions in a cookie.
|
||||
func DefaultSessionProvider(opts Options) CookieSessionProvider {
|
||||
cookieName := opts.CookieName
|
||||
if cookieName == "" {
|
||||
cookieName = defaultSessionCookieName
|
||||
}
|
||||
return CookieSessionProvider{
|
||||
Name: cookieName,
|
||||
Domain: opts.URL.Host,
|
||||
MaxAge: defaultSessionMaxAge,
|
||||
HTTPOnly: true,
|
||||
Secure: opts.URL.Scheme == "https",
|
||||
SameSite: opts.CookieSameSite,
|
||||
Codec: DefaultSessionCodec(opts),
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultTrackedRequestCodec returns a new TrackedRequestCodec for the provided
|
||||
// options, a JWTTrackedRequestCodec that uses a JWT to encode TrackedRequests.
|
||||
func DefaultTrackedRequestCodec(opts Options) JWTTrackedRequestCodec {
|
||||
return JWTTrackedRequestCodec{
|
||||
SigningMethod: defaultJWTSigningMethod,
|
||||
Audience: opts.URL.String(),
|
||||
Issuer: opts.URL.String(),
|
||||
MaxAge: saml.MaxIssueDelay,
|
||||
Key: opts.Key,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultRequestTracker returns a new RequestTracker for the provided options,
|
||||
// a CookieRequestTracker which uses cookies to track pending requests.
|
||||
func DefaultRequestTracker(opts Options, serviceProvider *saml.ServiceProvider) CookieRequestTracker {
|
||||
return CookieRequestTracker{
|
||||
ServiceProvider: serviceProvider,
|
||||
NamePrefix: "saml_",
|
||||
Codec: DefaultTrackedRequestCodec(opts),
|
||||
MaxAge: saml.MaxIssueDelay,
|
||||
RelayStateFunc: opts.RelayStateFunc,
|
||||
SameSite: opts.CookieSameSite,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultServiceProvider returns the default saml.ServiceProvider for the provided
|
||||
// options.
|
||||
func DefaultServiceProvider(opts Options) saml.ServiceProvider {
|
||||
metadataURL := opts.URL.ResolveReference(&url.URL{Path: "saml/metadata"})
|
||||
acsURL := opts.URL.ResolveReference(&url.URL{Path: "saml/acs"})
|
||||
sloURL := opts.URL.ResolveReference(&url.URL{Path: "saml/slo"})
|
||||
|
||||
var forceAuthn *bool
|
||||
if opts.ForceAuthn {
|
||||
forceAuthn = &opts.ForceAuthn
|
||||
}
|
||||
signatureMethod := dsig.RSASHA1SignatureMethod
|
||||
if !opts.SignRequest {
|
||||
signatureMethod = ""
|
||||
}
|
||||
|
||||
if opts.DefaultRedirectURI == "" {
|
||||
opts.DefaultRedirectURI = "/"
|
||||
}
|
||||
|
||||
if len(opts.LogoutBindings) == 0 {
|
||||
opts.LogoutBindings = []string{saml.HTTPPostBinding}
|
||||
}
|
||||
|
||||
return saml.ServiceProvider{
|
||||
EntityID: opts.EntityID,
|
||||
Key: opts.Key,
|
||||
Certificate: opts.Certificate,
|
||||
HTTPClient: opts.HTTPClient,
|
||||
Intermediates: opts.Intermediates,
|
||||
MetadataURL: *metadataURL,
|
||||
AcsURL: *acsURL,
|
||||
SloURL: *sloURL,
|
||||
IDPMetadata: opts.IDPMetadata,
|
||||
ForceAuthn: forceAuthn,
|
||||
RequestedAuthnContext: opts.RequestedAuthnContext,
|
||||
SignatureMethod: signatureMethod,
|
||||
AllowIDPInitiated: opts.AllowIDPInitiated,
|
||||
DefaultRedirectURI: opts.DefaultRedirectURI,
|
||||
LogoutBindings: opts.LogoutBindings,
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new Middleware with the default providers for the
|
||||
// given options.
|
||||
//
|
||||
// You can customize the behavior of the middleware in more detail by
|
||||
// replacing and/or changing Session, RequestTracker, and ServiceProvider
|
||||
// in the returned Middleware.
|
||||
func New(opts Options) (*Middleware, error) {
|
||||
m := &Middleware{
|
||||
ServiceProvider: DefaultServiceProvider(opts),
|
||||
Binding: "",
|
||||
ResponseBinding: saml.HTTPPostBinding,
|
||||
OnError: DefaultOnError,
|
||||
Session: DefaultSessionProvider(opts),
|
||||
}
|
||||
m.RequestTracker = DefaultRequestTracker(opts, &m.ServiceProvider)
|
||||
if opts.UseArtifactResponse {
|
||||
m.ResponseBinding = saml.HTTPArtifactBinding
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package samlsp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// RequestTracker tracks pending authentication requests.
|
||||
//
|
||||
// There are two main reasons for this:
|
||||
//
|
||||
// 1. When the middleware initiates an authentication request it must track the original URL
|
||||
// in order to redirect the user to the right place after the authentication completes.
|
||||
//
|
||||
// 2. After the authentication completes, we want to ensure that the user presenting the
|
||||
// assertion is actually the one the request it, to mitigate request forgeries.
|
||||
type RequestTracker interface {
|
||||
// TrackRequest starts tracking the SAML request with the given ID. It returns an
|
||||
// `index` that should be used as the RelayState in the SAMl request flow.
|
||||
TrackRequest(w http.ResponseWriter, r *http.Request, samlRequestID string) (index string, err error)
|
||||
|
||||
// StopTrackingRequest stops tracking the SAML request given by index, which is a string
|
||||
// previously returned from TrackRequest
|
||||
StopTrackingRequest(w http.ResponseWriter, r *http.Request, index string) error
|
||||
|
||||
// GetTrackedRequests returns all the pending tracked requests
|
||||
GetTrackedRequests(r *http.Request) []TrackedRequest
|
||||
|
||||
// GetTrackedRequest returns a pending tracked request.
|
||||
GetTrackedRequest(r *http.Request, index string) (*TrackedRequest, error)
|
||||
}
|
||||
|
||||
// TrackedRequest holds the data we store for each pending request.
|
||||
type TrackedRequest struct {
|
||||
Index string `json:"-"`
|
||||
SAMLRequestID string `json:"id"`
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
// TrackedRequestCodec handles encoding and decoding of a TrackedRequest.
|
||||
type TrackedRequestCodec interface {
|
||||
// Encode returns an encoded string representing the TrackedRequest.
|
||||
Encode(value TrackedRequest) (string, error)
|
||||
|
||||
// Decode returns a Tracked request from an encoded string.
|
||||
Decode(signed string) (*TrackedRequest, error)
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package samlsp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
)
|
||||
|
||||
var _ RequestTracker = CookieRequestTracker{}
|
||||
|
||||
// CookieRequestTracker tracks requests by setting a uniquely named
|
||||
// cookie for each request.
|
||||
type CookieRequestTracker struct {
|
||||
ServiceProvider *saml.ServiceProvider
|
||||
NamePrefix string
|
||||
Codec TrackedRequestCodec
|
||||
MaxAge time.Duration
|
||||
RelayStateFunc func(w http.ResponseWriter, r *http.Request) string
|
||||
SameSite http.SameSite
|
||||
}
|
||||
|
||||
// TrackRequest starts tracking the SAML request with the given ID. It returns an
|
||||
// `index` that should be used as the RelayState in the SAMl request flow.
|
||||
func (t CookieRequestTracker) TrackRequest(w http.ResponseWriter, r *http.Request, samlRequestID string) (string, error) {
|
||||
trackedRequest := TrackedRequest{
|
||||
Index: base64.RawURLEncoding.EncodeToString(randomBytes(42)),
|
||||
SAMLRequestID: samlRequestID,
|
||||
URI: r.URL.String(),
|
||||
}
|
||||
|
||||
if t.RelayStateFunc != nil {
|
||||
relayState := t.RelayStateFunc(w, r)
|
||||
if relayState != "" {
|
||||
trackedRequest.Index = relayState
|
||||
}
|
||||
}
|
||||
|
||||
signedTrackedRequest, err := t.Codec.Encode(trackedRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: t.NamePrefix + trackedRequest.Index,
|
||||
Value: signedTrackedRequest,
|
||||
MaxAge: int(t.MaxAge.Seconds()),
|
||||
HttpOnly: true,
|
||||
SameSite: t.SameSite,
|
||||
Secure: t.ServiceProvider.AcsURL.Scheme == "https",
|
||||
Path: t.ServiceProvider.AcsURL.Path,
|
||||
})
|
||||
|
||||
return trackedRequest.Index, nil
|
||||
}
|
||||
|
||||
// StopTrackingRequest stops tracking the SAML request given by index, which is a string
|
||||
// previously returned from TrackRequest
|
||||
func (t CookieRequestTracker) StopTrackingRequest(w http.ResponseWriter, r *http.Request, index string) error {
|
||||
cookie, err := r.Cookie(t.NamePrefix + index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cookie.Value = ""
|
||||
cookie.Domain = t.ServiceProvider.AcsURL.Hostname()
|
||||
cookie.Expires = time.Unix(1, 0) // past time as close to epoch as possible, but not zero time.Time{}
|
||||
http.SetCookie(w, cookie)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTrackedRequests returns all the pending tracked requests
|
||||
func (t CookieRequestTracker) GetTrackedRequests(r *http.Request) []TrackedRequest {
|
||||
rv := []TrackedRequest{}
|
||||
for _, cookie := range r.Cookies() {
|
||||
if !strings.HasPrefix(cookie.Name, t.NamePrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
trackedRequest, err := t.Codec.Decode(cookie.Value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
index := strings.TrimPrefix(cookie.Name, t.NamePrefix)
|
||||
if index != trackedRequest.Index {
|
||||
continue
|
||||
}
|
||||
|
||||
rv = append(rv, *trackedRequest)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
// GetTrackedRequest returns a pending tracked request.
|
||||
func (t CookieRequestTracker) GetTrackedRequest(r *http.Request, index string) (*TrackedRequest, error) {
|
||||
cookie, err := r.Cookie(t.NamePrefix + index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trackedRequest, err := t.Codec.Decode(cookie.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if trackedRequest.Index != index {
|
||||
return nil, fmt.Errorf("expected index %q, got %q", index, trackedRequest.Index)
|
||||
}
|
||||
return trackedRequest, nil
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package samlsp
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
)
|
||||
|
||||
var defaultJWTSigningMethod = jwt.SigningMethodRS256
|
||||
|
||||
// JWTTrackedRequestCodec encodes TrackedRequests as signed JWTs
|
||||
type JWTTrackedRequestCodec struct {
|
||||
SigningMethod jwt.SigningMethod
|
||||
Audience string
|
||||
Issuer string
|
||||
MaxAge time.Duration
|
||||
Key *rsa.PrivateKey
|
||||
}
|
||||
|
||||
var _ TrackedRequestCodec = JWTTrackedRequestCodec{}
|
||||
|
||||
// JWTTrackedRequestClaims represents the JWT claims for a tracked request.
|
||||
type JWTTrackedRequestClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
TrackedRequest
|
||||
SAMLAuthnRequest bool `json:"saml-authn-request"`
|
||||
}
|
||||
|
||||
// Encode returns an encoded string representing the TrackedRequest.
|
||||
func (s JWTTrackedRequestCodec) Encode(value TrackedRequest) (string, error) {
|
||||
now := saml.TimeNow()
|
||||
claims := JWTTrackedRequestClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Audience: jwt.ClaimStrings{s.Audience},
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(s.MaxAge)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
Issuer: s.Issuer,
|
||||
NotBefore: jwt.NewNumericDate(now), // TODO(ross): correct for clock skew
|
||||
Subject: value.Index,
|
||||
},
|
||||
TrackedRequest: value,
|
||||
SAMLAuthnRequest: true,
|
||||
}
|
||||
token := jwt.NewWithClaims(s.SigningMethod, claims)
|
||||
return token.SignedString(s.Key)
|
||||
}
|
||||
|
||||
// Decode returns a Tracked request from an encoded string.
|
||||
func (s JWTTrackedRequestCodec) Decode(signed string) (*TrackedRequest, error) {
|
||||
parser := jwt.Parser{
|
||||
ValidMethods: []string{s.SigningMethod.Alg()},
|
||||
}
|
||||
claims := JWTTrackedRequestClaims{}
|
||||
_, err := parser.ParseWithClaims(signed, &claims, func(*jwt.Token) (interface{}, error) {
|
||||
return s.Key.Public(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !claims.VerifyAudience(s.Audience, true) {
|
||||
return nil, fmt.Errorf("expected audience %q, got %q", s.Audience, claims.Audience)
|
||||
}
|
||||
if !claims.VerifyIssuer(s.Issuer, true) {
|
||||
return nil, fmt.Errorf("expected issuer %q, got %q", s.Issuer, claims.Issuer)
|
||||
}
|
||||
if !claims.SAMLAuthnRequest {
|
||||
return nil, fmt.Errorf("expected saml-authn-request")
|
||||
}
|
||||
claims.TrackedRequest.Index = claims.Subject
|
||||
return &claims.TrackedRequest, nil
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package samlsp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
)
|
||||
|
||||
// Session is an interface implemented to contain a session.
|
||||
type Session interface{}
|
||||
|
||||
// SessionWithAttributes is a session that can expose the
|
||||
// attributes provided by the SAML identity provider.
|
||||
type SessionWithAttributes interface {
|
||||
Session
|
||||
GetAttributes() Attributes
|
||||
}
|
||||
|
||||
// ErrNoSession is the error returned when the remote user does not have a session
|
||||
var ErrNoSession = errors.New("saml: session not present")
|
||||
|
||||
// SessionProvider is an interface implemented by types that can track
|
||||
// the active session of a user. The default implementation is CookieSessionProvider
|
||||
type SessionProvider interface {
|
||||
// CreateSession is called when we have received a valid SAML assertion and
|
||||
// should create a new session and modify the http response accordingly, e.g. by
|
||||
// setting a cookie.
|
||||
CreateSession(w http.ResponseWriter, r *http.Request, assertion *saml.Assertion) error
|
||||
|
||||
// DeleteSession is called to modify the response such that it removed the current
|
||||
// session, e.g. by deleting a cookie.
|
||||
DeleteSession(w http.ResponseWriter, r *http.Request) error
|
||||
|
||||
// GetSession returns the current Session associated with the request, or
|
||||
// ErrNoSession if there is no valid session.
|
||||
GetSession(r *http.Request) (Session, error)
|
||||
}
|
||||
|
||||
// SessionCodec is an interface to convert SAML assertions to a
|
||||
// Session. The default implementation uses JWTs, JWTSessionCodec.
|
||||
type SessionCodec interface {
|
||||
// New creates a Session from the SAML assertion.
|
||||
New(assertion *saml.Assertion) (Session, error)
|
||||
|
||||
// Encode returns a serialized version of the Session.
|
||||
//
|
||||
// Note: When implementing this function, it is reasonable to expect that
|
||||
// Session is of the exact type returned by New(), and panic if it is not.
|
||||
Encode(s Session) (string, error)
|
||||
|
||||
// Decode parses the serialized session that may have been returned by Encode
|
||||
// and returns a Session.
|
||||
Decode(string) (Session, error)
|
||||
}
|
||||
|
||||
type indexType int
|
||||
|
||||
const sessionIndex indexType = iota
|
||||
|
||||
// SessionFromContext returns the session associated with ctx, or nil
|
||||
// if no session are associated
|
||||
func SessionFromContext(ctx context.Context) Session {
|
||||
v := ctx.Value(sessionIndex)
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return v.(Session)
|
||||
}
|
||||
|
||||
// ContextWithSession returns a new context with session associated
|
||||
func ContextWithSession(ctx context.Context, session Session) context.Context {
|
||||
return context.WithValue(ctx, sessionIndex, session)
|
||||
}
|
||||
|
||||
// AttributeFromContext is a convenience method that returns the named attribute
|
||||
// from the session, if available.
|
||||
func AttributeFromContext(ctx context.Context, name string) string {
|
||||
s := SessionFromContext(ctx)
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
sa, ok := s.(SessionWithAttributes)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return sa.GetAttributes().Get(name)
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package samlsp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
)
|
||||
|
||||
const defaultSessionCookieName = "token"
|
||||
|
||||
var _ SessionProvider = CookieSessionProvider{}
|
||||
|
||||
// CookieSessionProvider is an implementation of SessionProvider that stores
|
||||
// session tokens in an HTTP cookie.
|
||||
type CookieSessionProvider struct {
|
||||
Name string
|
||||
Domain string
|
||||
HTTPOnly bool
|
||||
Secure bool
|
||||
SameSite http.SameSite
|
||||
MaxAge time.Duration
|
||||
Codec SessionCodec
|
||||
}
|
||||
|
||||
// CreateSession is called when we have received a valid SAML assertion and
|
||||
// should create a new session and modify the http response accordingly, e.g. by
|
||||
// setting a cookie.
|
||||
func (c CookieSessionProvider) CreateSession(w http.ResponseWriter, r *http.Request, assertion *saml.Assertion) error {
|
||||
// Cookies should not have the port attached to them so strip it off
|
||||
if domain, _, err := net.SplitHostPort(c.Domain); err == nil {
|
||||
c.Domain = domain
|
||||
}
|
||||
|
||||
session, err := c.Codec.New(assertion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value, err := c.Codec.Encode(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: c.Name,
|
||||
Domain: c.Domain,
|
||||
Value: value,
|
||||
MaxAge: int(c.MaxAge.Seconds()),
|
||||
HttpOnly: c.HTTPOnly,
|
||||
Secure: c.Secure || r.URL.Scheme == "https",
|
||||
SameSite: c.SameSite,
|
||||
Path: "/",
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSession is called to modify the response such that it removed the current
|
||||
// session, e.g. by deleting a cookie.
|
||||
func (c CookieSessionProvider) DeleteSession(w http.ResponseWriter, r *http.Request) error {
|
||||
// Cookies should not have the port attached to them so strip it off
|
||||
if domain, _, err := net.SplitHostPort(c.Domain); err == nil {
|
||||
c.Domain = domain
|
||||
}
|
||||
|
||||
cookie, err := r.Cookie(c.Name)
|
||||
|
||||
if err == http.ErrNoCookie {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cookie.Value = ""
|
||||
cookie.Expires = time.Unix(1, 0) // past time as close to epoch as possible, but not zero time.Time{}
|
||||
cookie.Path = "/"
|
||||
cookie.Domain = c.Domain
|
||||
http.SetCookie(w, cookie)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSession returns the current Session associated with the request, or
|
||||
// ErrNoSession if there is no valid session.
|
||||
func (c CookieSessionProvider) GetSession(r *http.Request) (Session, error) {
|
||||
cookie, err := r.Cookie(c.Name)
|
||||
if err == http.ErrNoCookie {
|
||||
return nil, ErrNoSession
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, err := c.Codec.Decode(cookie.Value)
|
||||
if err != nil {
|
||||
return nil, ErrNoSession
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package samlsp
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSessionMaxAge = time.Hour
|
||||
claimNameSessionIndex = "SessionIndex"
|
||||
)
|
||||
|
||||
// JWTSessionCodec implements SessionCoded to encode and decode Sessions from
|
||||
// the corresponding JWT.
|
||||
type JWTSessionCodec struct {
|
||||
SigningMethod jwt.SigningMethod
|
||||
Audience string
|
||||
Issuer string
|
||||
MaxAge time.Duration
|
||||
Key *rsa.PrivateKey
|
||||
}
|
||||
|
||||
var _ SessionCodec = JWTSessionCodec{}
|
||||
|
||||
// New creates a Session from the SAML assertion.
|
||||
//
|
||||
// The returned Session is a JWTSessionClaims.
|
||||
func (c JWTSessionCodec) New(assertion *saml.Assertion) (Session, error) {
|
||||
now := saml.TimeNow()
|
||||
claims := JWTSessionClaims{}
|
||||
claims.SAMLSession = true
|
||||
claims.Audience = c.Audience
|
||||
claims.Issuer = c.Issuer
|
||||
claims.IssuedAt = now.Unix()
|
||||
claims.ExpiresAt = now.Add(c.MaxAge).Unix()
|
||||
claims.NotBefore = now.Unix()
|
||||
|
||||
if sub := assertion.Subject; sub != nil {
|
||||
if nameID := sub.NameID; nameID != nil {
|
||||
claims.Subject = nameID.Value
|
||||
}
|
||||
}
|
||||
|
||||
claims.Attributes = map[string][]string{}
|
||||
|
||||
for _, attributeStatement := range assertion.AttributeStatements {
|
||||
for _, attr := range attributeStatement.Attributes {
|
||||
claimName := attr.FriendlyName
|
||||
if claimName == "" {
|
||||
claimName = attr.Name
|
||||
}
|
||||
for _, value := range attr.Values {
|
||||
claims.Attributes[claimName] = append(claims.Attributes[claimName], value.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add SessionIndex to claims Attributes
|
||||
for _, authnStatement := range assertion.AuthnStatements {
|
||||
claims.Attributes[claimNameSessionIndex] = append(claims.Attributes[claimNameSessionIndex],
|
||||
authnStatement.SessionIndex)
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// Encode returns a serialized version of the Session.
|
||||
//
|
||||
// The provided session must be a JWTSessionClaims, otherwise this
|
||||
// function will panic.
|
||||
func (c JWTSessionCodec) Encode(s Session) (string, error) {
|
||||
claims := s.(JWTSessionClaims) // this will panic if you pass the wrong kind of session
|
||||
|
||||
token := jwt.NewWithClaims(c.SigningMethod, claims)
|
||||
signedString, err := token.SignedString(c.Key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return signedString, nil
|
||||
}
|
||||
|
||||
// Decode parses the serialized session that may have been returned by Encode
|
||||
// and returns a Session.
|
||||
func (c JWTSessionCodec) Decode(signed string) (Session, error) {
|
||||
parser := jwt.Parser{
|
||||
ValidMethods: []string{c.SigningMethod.Alg()},
|
||||
}
|
||||
claims := JWTSessionClaims{}
|
||||
_, err := parser.ParseWithClaims(signed, &claims, func(*jwt.Token) (interface{}, error) {
|
||||
return c.Key.Public(), nil
|
||||
})
|
||||
// TODO(ross): check for errors due to bad time and return ErrNoSession
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !claims.VerifyAudience(c.Audience, true) {
|
||||
return nil, fmt.Errorf("expected audience %q, got %q", c.Audience, claims.Audience)
|
||||
}
|
||||
if !claims.VerifyIssuer(c.Issuer, true) {
|
||||
return nil, fmt.Errorf("expected issuer %q, got %q", c.Issuer, claims.Issuer)
|
||||
}
|
||||
if !claims.SAMLSession {
|
||||
return nil, errors.New("expected saml-session")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// JWTSessionClaims represents the JWT claims in the encoded session
|
||||
type JWTSessionClaims struct {
|
||||
jwt.StandardClaims
|
||||
Attributes Attributes `json:"attr"`
|
||||
SAMLSession bool `json:"saml-session"`
|
||||
}
|
||||
|
||||
var _ Session = JWTSessionClaims{}
|
||||
|
||||
// GetAttributes implements SessionWithAttributes. It returns the SAMl attributes.
|
||||
func (c JWTSessionClaims) GetAttributes() Attributes {
|
||||
return c.Attributes
|
||||
}
|
||||
|
||||
// Attributes is a map of attributes provided in the SAML assertion
|
||||
type Attributes map[string][]string
|
||||
|
||||
// Get returns the first attribute named `key` or an empty string if
|
||||
// no such attributes is present.
|
||||
func (a Attributes) Get(key string) string {
|
||||
if a == nil {
|
||||
return ""
|
||||
}
|
||||
v := a[key]
|
||||
if len(v) == 0 {
|
||||
return ""
|
||||
}
|
||||
return v[0]
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package samlsp
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
)
|
||||
|
||||
func randomBytes(n int) []byte {
|
||||
rv := make([]byte, n)
|
||||
|
||||
if _, err := io.ReadFull(saml.RandReader, rv); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
+1321
File diff suppressed because it is too large
Load Diff
+1696
File diff suppressed because it is too large
Load Diff
+52
@@ -0,0 +1,52 @@
|
||||
package saml
|
||||
|
||||
import "time"
|
||||
|
||||
// RelaxedTime is a version of time.Time that supports the time format
|
||||
// found in SAML documents.
|
||||
type RelaxedTime time.Time
|
||||
|
||||
const timeFormat = "2006-01-02T15:04:05.999Z07:00"
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler
|
||||
func (m RelaxedTime) MarshalText() ([]byte, error) {
|
||||
// According to section 1.2.2 of the OASIS SAML 1.1 spec, we can't trust
|
||||
// other applications to handle time resolution finer than a millisecond.
|
||||
//
|
||||
// The time MUST be expressed in UTC.
|
||||
return []byte(m.String()), nil
|
||||
}
|
||||
|
||||
func (m RelaxedTime) String() string {
|
||||
return time.Time(m).Round(time.Millisecond).UTC().Format(timeFormat)
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler
|
||||
func (m *RelaxedTime) UnmarshalText(text []byte) error {
|
||||
if len(text) == 0 {
|
||||
*m = RelaxedTime(time.Time{})
|
||||
return nil
|
||||
}
|
||||
t, err1 := time.Parse(time.RFC3339, string(text))
|
||||
if err1 == nil {
|
||||
t = t.Round(time.Millisecond)
|
||||
*m = RelaxedTime(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
t, err2 := time.Parse(time.RFC3339Nano, string(text))
|
||||
if err2 == nil {
|
||||
t = t.Round(time.Millisecond)
|
||||
*m = RelaxedTime(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
t, err2 = time.Parse("2006-01-02T15:04:05.999999999", string(text))
|
||||
if err2 == nil {
|
||||
t = t.Round(time.Millisecond)
|
||||
*m = RelaxedTime(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
return err1
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
dsig "github.com/russellhaering/goxmldsig"
|
||||
)
|
||||
|
||||
// TimeNow is a function that returns the current time. The default
|
||||
// value is time.Now, but it can be replaced for testing.
|
||||
var TimeNow = func() time.Time { return time.Now().UTC() }
|
||||
|
||||
// Clock is assigned to dsig validation and signing contexts if it is
|
||||
// not nil, otherwise the default clock is used.
|
||||
var Clock *dsig.Clock
|
||||
|
||||
// RandReader is the io.Reader that produces cryptographically random
|
||||
// bytes when they are need by the library. The default value is
|
||||
// rand.Reader, but it can be replaced for testing.
|
||||
var RandReader = rand.Reader
|
||||
|
||||
//nolint:unparam // This always receives 20, but we want the option to do more or less if needed.
|
||||
func randomBytes(n int) []byte {
|
||||
rv := make([]byte, n)
|
||||
|
||||
if _, err := io.ReadFull(RandReader, rv); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package xmlenc
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/des" // nolint: gas
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
// CBC implements Decrypter and Encrypter for block ciphers in CBC mode
|
||||
type CBC struct {
|
||||
keySize int
|
||||
algorithm string
|
||||
cipher func([]byte) (cipher.Block, error)
|
||||
}
|
||||
|
||||
// KeySize returns the length of the key required.
|
||||
func (e CBC) KeySize() int {
|
||||
return e.keySize
|
||||
}
|
||||
|
||||
// Algorithm returns the name of the algorithm, as will be found
|
||||
// in an xenc:EncryptionMethod element.
|
||||
func (e CBC) Algorithm() string {
|
||||
return e.algorithm
|
||||
}
|
||||
|
||||
// Encrypt encrypts plaintext with key, which should be a []byte of length KeySize().
|
||||
// It returns an xenc:EncryptedData element.
|
||||
func (e CBC) Encrypt(key interface{}, plaintext []byte, _ []byte) (*etree.Element, error) {
|
||||
keyBuf, ok := key.([]byte)
|
||||
if !ok {
|
||||
return nil, ErrIncorrectKeyType("[]byte")
|
||||
}
|
||||
if len(keyBuf) != e.keySize {
|
||||
return nil, ErrIncorrectKeyLength(e.keySize)
|
||||
}
|
||||
|
||||
block, err := e.cipher(keyBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encryptedDataEl := etree.NewElement("xenc:EncryptedData")
|
||||
encryptedDataEl.CreateAttr("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#")
|
||||
{
|
||||
randBuf := make([]byte, 16)
|
||||
if _, err := RandReader.Read(randBuf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encryptedDataEl.CreateAttr("Id", fmt.Sprintf("_%x", randBuf))
|
||||
}
|
||||
|
||||
em := encryptedDataEl.CreateElement("xenc:EncryptionMethod")
|
||||
em.CreateAttr("Algorithm", e.algorithm)
|
||||
em.CreateAttr("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#")
|
||||
|
||||
plaintext = appendPadding(plaintext, block.BlockSize())
|
||||
|
||||
iv := make([]byte, block.BlockSize())
|
||||
if _, err := RandReader.Read(iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
ciphertext := make([]byte, len(plaintext))
|
||||
mode.CryptBlocks(ciphertext, plaintext)
|
||||
ciphertext = append(iv, ciphertext...)
|
||||
|
||||
cd := encryptedDataEl.CreateElement("xenc:CipherData")
|
||||
cd.CreateAttr("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#")
|
||||
cd.CreateElement("xenc:CipherValue").SetText(base64.StdEncoding.EncodeToString(ciphertext))
|
||||
return encryptedDataEl, nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts an encrypted element with key. If the ciphertext contains an
|
||||
// EncryptedKey element, then the type of `key` is determined by the registered
|
||||
// Decryptor for the EncryptedKey element. Otherwise, `key` must be a []byte of
|
||||
// length KeySize().
|
||||
func (e CBC) Decrypt(key interface{}, ciphertextEl *etree.Element) ([]byte, error) {
|
||||
// If the key is encrypted, decrypt it.
|
||||
if encryptedKeyEl := ciphertextEl.FindElement("./KeyInfo/EncryptedKey"); encryptedKeyEl != nil {
|
||||
var err error
|
||||
key, err = Decrypt(key, encryptedKeyEl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
keyBuf, ok := key.([]byte)
|
||||
if !ok {
|
||||
return nil, ErrIncorrectKeyType("[]byte")
|
||||
}
|
||||
if len(keyBuf) != e.KeySize() {
|
||||
return nil, ErrIncorrectKeyLength(e.KeySize())
|
||||
}
|
||||
|
||||
block, err := e.cipher(keyBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ciphertext, err := getCiphertext(ciphertextEl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ciphertext) < block.BlockSize() {
|
||||
return nil, errors.New("ciphertext too short")
|
||||
}
|
||||
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
mode.CryptBlocks(plaintext, ciphertext) // decrypt in place
|
||||
|
||||
plaintext, err = stripPadding(plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
var (
|
||||
// AES128CBC implements AES128-CBC symetric key mode for encryption and decryption
|
||||
AES128CBC BlockCipher = CBC{
|
||||
keySize: 16,
|
||||
algorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc",
|
||||
cipher: aes.NewCipher,
|
||||
}
|
||||
|
||||
// AES192CBC implements AES192-CBC symetric key mode for encryption and decryption
|
||||
AES192CBC BlockCipher = CBC{
|
||||
keySize: 24,
|
||||
algorithm: "http://www.w3.org/2001/04/xmlenc#aes192-cbc",
|
||||
cipher: aes.NewCipher,
|
||||
}
|
||||
|
||||
// AES256CBC implements AES256-CBC symetric key mode for encryption and decryption
|
||||
AES256CBC BlockCipher = CBC{
|
||||
keySize: 32,
|
||||
algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc",
|
||||
cipher: aes.NewCipher,
|
||||
}
|
||||
|
||||
// TripleDES implements 3DES in CBC mode for encryption and decryption
|
||||
TripleDES BlockCipher = CBC{
|
||||
keySize: 8,
|
||||
algorithm: "http://www.w3.org/2001/04/xmlenc#tripledes-cbc",
|
||||
cipher: des.NewCipher,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterDecrypter(AES128CBC)
|
||||
RegisterDecrypter(AES192CBC)
|
||||
RegisterDecrypter(AES256CBC)
|
||||
RegisterDecrypter(TripleDES)
|
||||
}
|
||||
|
||||
func appendPadding(buf []byte, blockSize int) []byte {
|
||||
paddingBytes := blockSize - (len(buf) % blockSize)
|
||||
padding := make([]byte, paddingBytes)
|
||||
padding[len(padding)-1] = byte(paddingBytes)
|
||||
return append(buf, padding...)
|
||||
}
|
||||
|
||||
func stripPadding(buf []byte) ([]byte, error) {
|
||||
if len(buf) < 1 {
|
||||
return nil, errors.New("buffer is too short for padding")
|
||||
}
|
||||
paddingBytes := int(buf[len(buf)-1])
|
||||
if paddingBytes > len(buf)-1 {
|
||||
return nil, errors.New("buffer is too short for padding")
|
||||
}
|
||||
if paddingBytes < 1 {
|
||||
return nil, errors.New("padding must be at least one byte")
|
||||
}
|
||||
return buf[:len(buf)-paddingBytes], nil
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package xmlenc
|
||||
|
||||
import (
|
||||
|
||||
// nolint: gas
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
// ErrAlgorithmNotImplemented is returned when encryption used is not
|
||||
// supported.
|
||||
type ErrAlgorithmNotImplemented string
|
||||
|
||||
func (e ErrAlgorithmNotImplemented) Error() string {
|
||||
return "algorithm is not implemented: " + string(e)
|
||||
}
|
||||
|
||||
// ErrCannotFindRequiredElement is returned by Decrypt when a required
|
||||
// element cannot be found.
|
||||
type ErrCannotFindRequiredElement string
|
||||
|
||||
func (e ErrCannotFindRequiredElement) Error() string {
|
||||
return "cannot find required element: " + string(e)
|
||||
}
|
||||
|
||||
// ErrIncorrectTag is returned when Decrypt is passed an element which
|
||||
// is neither an EncryptedType nor an EncryptedKey
|
||||
var ErrIncorrectTag = fmt.Errorf("tag must be an EncryptedType or EncryptedKey")
|
||||
|
||||
// ErrIncorrectKeyLength is returned when the fixed length key is not
|
||||
// of the required length.
|
||||
type ErrIncorrectKeyLength int
|
||||
|
||||
func (e ErrIncorrectKeyLength) Error() string {
|
||||
return fmt.Sprintf("expected key to be %d bytes", int(e))
|
||||
}
|
||||
|
||||
// ErrIncorrectKeyType is returned when the key is not the correct type
|
||||
type ErrIncorrectKeyType string
|
||||
|
||||
func (e ErrIncorrectKeyType) Error() string {
|
||||
return fmt.Sprintf("expected key to be %s", string(e))
|
||||
}
|
||||
|
||||
// Decrypt decrypts the encrypted data using the provided key. If the
|
||||
// data are encrypted using AES or 3DEC, then the key should be a []byte.
|
||||
// If the data are encrypted with PKCS1v15 or RSA-OAEP-MGF1P then key should
|
||||
// be a *rsa.PrivateKey.
|
||||
func Decrypt(key interface{}, ciphertextEl *etree.Element) ([]byte, error) {
|
||||
encryptionMethodEl := ciphertextEl.FindElement("./EncryptionMethod")
|
||||
if encryptionMethodEl == nil {
|
||||
return nil, ErrCannotFindRequiredElement("EncryptionMethod")
|
||||
}
|
||||
algorithm := encryptionMethodEl.SelectAttrValue("Algorithm", "")
|
||||
decrypter, ok := decrypters[algorithm]
|
||||
if !ok {
|
||||
return nil, ErrAlgorithmNotImplemented(algorithm)
|
||||
}
|
||||
return decrypter.Decrypt(key, ciphertextEl)
|
||||
}
|
||||
|
||||
func getCiphertext(encryptedKey *etree.Element) ([]byte, error) {
|
||||
ciphertextEl := encryptedKey.FindElement("./CipherData/CipherValue")
|
||||
if ciphertextEl == nil {
|
||||
return nil, fmt.Errorf("cannot find CipherData element containing a CipherValue element")
|
||||
}
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(strings.TrimSpace(ciphertextEl.Text()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// extract and verify that the public key matches the certificate
|
||||
// this section is included to either let the service know up front
|
||||
// if the key will work, or let the service provider know which key
|
||||
// to use to decrypt the message. Either way, verification is not
|
||||
// security-critical.
|
||||
//nolint:revive,staticcheck // Keep the later empty branch so that we know to address this at a later date.
|
||||
if el := encryptedKey.FindElement("./KeyInfo/X509Data/X509Certificate"); el != nil {
|
||||
certPEMbuf := el.Text()
|
||||
certPEMbuf = "-----BEGIN CERTIFICATE-----\n" + certPEMbuf + "\n-----END CERTIFICATE-----\n"
|
||||
certPEM, _ := pem.Decode([]byte(certPEMbuf))
|
||||
if certPEM == nil {
|
||||
return nil, fmt.Errorf("invalid certificate")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(certPEM.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pubKey, ok := cert.PublicKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected certificate to be an *rsa.PublicKey")
|
||||
}
|
||||
if rsaKey.N.Cmp(pubKey.N) != 0 || rsaKey.E != pubKey.E {
|
||||
return nil, fmt.Errorf("certificate does not match provided key")
|
||||
}
|
||||
} else if el = encryptedKey.FindElement("./KeyInfo/X509Data/X509IssuerSerial"); el != nil {
|
||||
// TODO: determine how to validate the issuer serial information
|
||||
}
|
||||
return rsaKey, nil
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package xmlenc
|
||||
|
||||
import (
|
||||
"crypto/sha1" //nolint:gosec // required for protocol support
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"hash"
|
||||
|
||||
//nolint:staticcheck // We should support this for legacy reasons.
|
||||
"golang.org/x/crypto/ripemd160"
|
||||
)
|
||||
|
||||
type digestMethod struct {
|
||||
algorithm string
|
||||
hash func() hash.Hash
|
||||
}
|
||||
|
||||
func (dm digestMethod) Algorithm() string {
|
||||
return dm.algorithm
|
||||
}
|
||||
|
||||
func (dm digestMethod) Hash() hash.Hash {
|
||||
return dm.hash()
|
||||
}
|
||||
|
||||
var (
|
||||
// SHA1 implements the SHA-1 digest method (which is considered insecure)
|
||||
SHA1 = digestMethod{
|
||||
algorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
|
||||
hash: sha1.New,
|
||||
}
|
||||
|
||||
// SHA256 implements the SHA-256 digest method
|
||||
SHA256 = digestMethod{
|
||||
algorithm: "http://www.w3.org/2000/09/xmldsig#sha256",
|
||||
hash: sha256.New,
|
||||
}
|
||||
|
||||
// SHA512 implements the SHA-512 digest method
|
||||
SHA512 = digestMethod{
|
||||
algorithm: "http://www.w3.org/2000/09/xmldsig#sha512",
|
||||
hash: sha512.New,
|
||||
}
|
||||
|
||||
// RIPEMD160 implements the RIPEMD160 digest method
|
||||
RIPEMD160 = digestMethod{
|
||||
algorithm: "http://www.w3.org/2000/09/xmldsig#ripemd160",
|
||||
hash: ripemd160.New,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterDigestMethod(SHA1)
|
||||
RegisterDigestMethod(SHA256)
|
||||
RegisterDigestMethod(SHA512)
|
||||
RegisterDigestMethod(RIPEMD160)
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package xmlenc
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
var testKey = func() *rsa.PrivateKey {
|
||||
const keyStr = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQDkXTUsWzRVpUHjbDpWCfYDfXmQ/q4LkaioZoTpu4ut1Q3eQC5t
|
||||
gD14agJhgT8yzeY5S/YNlwCyuVkjuFyoyTHFX2IOPpz7jnh4KnQ+B1IH9fY/+kmk
|
||||
zHJgxSUDJsdUMPgGpKt5hnEn7ziXAWXLc2udFbnHwhi9TXXwRHGi9wZ4YwIDAQAB
|
||||
AoGBALNTnlXeqRI4W61DZ+v4ln/XIIeD9xiOoWrcVrNU2zL+g41ryQmkEqFkXcpD
|
||||
vGUg2xFTXTz+v0WZ1y39sIW6uKFRYUfaNsF6iVfGAyx1VWK/jgtPnCWDQy26Eby0
|
||||
BqpbZRy1a6MLYVEG/5bvZE01CDV4XttpTrNX91WWcYGduJxBAkEA6ED1ZOqIzBpu
|
||||
c2KAo+bWmroCH8+cSDk0gVq6bnRB+EEhRCmo/VgvndWLxfexdGmDIOAIisB06N5a
|
||||
GzBSCaEY/QJBAPu2cNvuuBNLwrlxPCwOEpIHYT4gJq8UMtg6O6N+u++nYCGhK6uo
|
||||
VCmrKY+UewyNIcsLZF0jsNI2qJjiU1vQxN8CQQDfQJnigMQwlfO3/Ga1po6Buu2R
|
||||
0IpkroB3G1R8GkrTrR+iGv2zUdKrwHsUOC2fPlFrB4+OeMOomRw6aG9jjDStAkB1
|
||||
ztiZhuvuVAoKIv5HnDqC0CNqIUAZtzlozDB3f+xT6SFr+/Plfn4Nlod4JMVGhZNo
|
||||
ZaeOlBLBAEX+cAcVtOs/AkBicZOAPv84ABmFfyhXhYaAuacaJLq//jg+t+URUOg+
|
||||
XZS9naRmawEQxOkZQVoMeKgvu05+V4MniFqdQBINIkr5
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
b, _ := pem.Decode([]byte(keyStr))
|
||||
k, err := x509.ParsePKCS1PrivateKey(b.Bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return k
|
||||
}()
|
||||
|
||||
// Fuzz is the go-fuzz fuzzing function
|
||||
func Fuzz(data []byte) int {
|
||||
doc := etree.NewDocument()
|
||||
if err := doc.ReadFromBytes(data); err != nil {
|
||||
return 0
|
||||
}
|
||||
if doc.Root() == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
if _, err := Decrypt(testKey, doc.Root()); err != nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package xmlenc
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
// GCM implements Decrypter and Encrypter for block ciphers in struct mode
|
||||
type GCM struct {
|
||||
keySize int
|
||||
algorithm string
|
||||
cipher func([]byte) (cipher.Block, error)
|
||||
}
|
||||
|
||||
// KeySize returns the length of the key required.
|
||||
func (e GCM) KeySize() int {
|
||||
return e.keySize
|
||||
}
|
||||
|
||||
// Algorithm returns the name of the algorithm, as will be found
|
||||
// in an xenc:EncryptionMethod element.
|
||||
func (e GCM) Algorithm() string {
|
||||
return e.algorithm
|
||||
}
|
||||
|
||||
// Encrypt encrypts plaintext with key and nonce
|
||||
func (e GCM) Encrypt(key interface{}, plaintext []byte, nonce []byte) (*etree.Element, error) {
|
||||
keyBuf, ok := key.([]byte)
|
||||
if !ok {
|
||||
return nil, ErrIncorrectKeyType("[]byte")
|
||||
}
|
||||
if len(keyBuf) != e.keySize {
|
||||
return nil, ErrIncorrectKeyLength(e.keySize)
|
||||
}
|
||||
|
||||
block, err := e.cipher(keyBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encryptedDataEl := etree.NewElement("xenc:EncryptedData")
|
||||
encryptedDataEl.CreateAttr("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#")
|
||||
{
|
||||
randBuf := make([]byte, 16)
|
||||
if _, err := RandReader.Read(randBuf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encryptedDataEl.CreateAttr("Id", fmt.Sprintf("_%x", randBuf))
|
||||
}
|
||||
|
||||
em := encryptedDataEl.CreateElement("xenc:EncryptionMethod")
|
||||
em.CreateAttr("Algorithm", e.algorithm)
|
||||
em.CreateAttr("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#")
|
||||
|
||||
plaintext = appendPadding(plaintext, block.BlockSize())
|
||||
|
||||
aesgcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if nonce == nil {
|
||||
// generate random nonce when it's nil
|
||||
nonce := make([]byte, aesgcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
ciphertext := make([]byte, len(plaintext))
|
||||
text := aesgcm.Seal(nil, nonce, ciphertext, nil)
|
||||
|
||||
cd := encryptedDataEl.CreateElement("xenc:CipherData")
|
||||
cd.CreateAttr("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#")
|
||||
cd.CreateElement("xenc:CipherValue").SetText(base64.StdEncoding.EncodeToString(text))
|
||||
return encryptedDataEl, nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts an encrypted element with key. If the ciphertext contains an
|
||||
// EncryptedKey element, then the type of `key` is determined by the registered
|
||||
// Decryptor for the EncryptedKey element. Otherwise, `key` must be a []byte of
|
||||
// length KeySize().
|
||||
func (e GCM) Decrypt(key interface{}, ciphertextEl *etree.Element) ([]byte, error) {
|
||||
if encryptedKeyEl := ciphertextEl.FindElement("./KeyInfo/EncryptedKey"); encryptedKeyEl != nil {
|
||||
var err error
|
||||
key, err = Decrypt(key, encryptedKeyEl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
keyBuf, ok := key.([]byte)
|
||||
|
||||
if !ok {
|
||||
return nil, ErrIncorrectKeyType("[]byte")
|
||||
}
|
||||
if len(keyBuf) != e.KeySize() {
|
||||
return nil, ErrIncorrectKeyLength(e.KeySize())
|
||||
}
|
||||
|
||||
block, err := e.cipher(keyBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aesgcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ciphertext, err := getCiphertext(ciphertextEl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := ciphertext[:aesgcm.NonceSize()]
|
||||
text := ciphertext[aesgcm.NonceSize():]
|
||||
|
||||
plainText, err := aesgcm.Open(nil, nonce, text, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plainText, nil
|
||||
}
|
||||
|
||||
var (
|
||||
// AES128GCM implements AES128-GCM mode for encryption and decryption
|
||||
AES128GCM BlockCipher = GCM{
|
||||
keySize: 16,
|
||||
algorithm: "http://www.w3.org/2009/xmlenc11#aes128-gcm",
|
||||
cipher: aes.NewCipher,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterDecrypter(AES128GCM)
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package xmlenc
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
// RSA implements Encrypter and Decrypter using RSA public key encryption.
|
||||
//
|
||||
// Use function like OAEP(), or PKCS1v15() to get an instance of this type ready
|
||||
// to use.
|
||||
type RSA struct {
|
||||
BlockCipher BlockCipher
|
||||
DigestMethod DigestMethod // only for OAEP
|
||||
|
||||
algorithm string
|
||||
keyEncrypter func(e RSA, pubKey *rsa.PublicKey, plaintext []byte) ([]byte, error)
|
||||
keyDecrypter func(e RSA, privKey *rsa.PrivateKey, ciphertext []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// Algorithm returns the name of the algorithm
|
||||
func (e RSA) Algorithm() string {
|
||||
return e.algorithm
|
||||
}
|
||||
|
||||
// Encrypt implements encrypter. certificate must be a []byte containing the ASN.1 bytes
|
||||
// of certificate containing an RSA public key.
|
||||
func (e RSA) Encrypt(certificate interface{}, plaintext []byte, nonce []byte) (*etree.Element, error) {
|
||||
cert, ok := certificate.(*x509.Certificate)
|
||||
if !ok {
|
||||
return nil, ErrIncorrectKeyType("*x.509 certificate")
|
||||
}
|
||||
|
||||
pubKey, ok := cert.PublicKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, ErrIncorrectKeyType("x.509 certificate with an RSA public key")
|
||||
}
|
||||
|
||||
// generate a key
|
||||
key := make([]byte, e.BlockCipher.KeySize())
|
||||
if _, err := RandReader.Read(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyInfoEl := etree.NewElement("ds:KeyInfo")
|
||||
keyInfoEl.CreateAttr("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
|
||||
|
||||
encryptedKey := keyInfoEl.CreateElement("xenc:EncryptedKey")
|
||||
{
|
||||
randBuf := make([]byte, 16)
|
||||
if _, err := RandReader.Read(randBuf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encryptedKey.CreateAttr("Id", fmt.Sprintf("_%x", randBuf))
|
||||
}
|
||||
encryptedKey.CreateAttr("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#")
|
||||
|
||||
encryptionMethodEl := encryptedKey.CreateElement("xenc:EncryptionMethod")
|
||||
encryptionMethodEl.CreateAttr("Algorithm", e.algorithm)
|
||||
encryptionMethodEl.CreateAttr("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#")
|
||||
if e.DigestMethod != nil {
|
||||
dm := encryptionMethodEl.CreateElement("ds:DigestMethod")
|
||||
dm.CreateAttr("Algorithm", e.DigestMethod.Algorithm())
|
||||
dm.CreateAttr("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
|
||||
}
|
||||
{
|
||||
innerKeyInfoEl := encryptedKey.CreateElement("ds:KeyInfo")
|
||||
x509data := innerKeyInfoEl.CreateElement("ds:X509Data")
|
||||
x509data.CreateElement("ds:X509Certificate").SetText(
|
||||
base64.StdEncoding.EncodeToString(cert.Raw),
|
||||
)
|
||||
}
|
||||
|
||||
buf, err := e.keyEncrypter(e, pubKey, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cd := encryptedKey.CreateElement("xenc:CipherData")
|
||||
cd.CreateAttr("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#")
|
||||
cd.CreateElement("xenc:CipherValue").SetText(base64.StdEncoding.EncodeToString(buf))
|
||||
encryptedDataEl, err := e.BlockCipher.Encrypt(key, plaintext, nonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encryptedDataEl.InsertChildAt(encryptedDataEl.FindElement("./CipherData").Index(), keyInfoEl)
|
||||
|
||||
return encryptedDataEl, nil
|
||||
}
|
||||
|
||||
// Decrypt implements Decryptor. `key` must be an *rsa.PrivateKey.
|
||||
func (e RSA) Decrypt(key interface{}, ciphertextEl *etree.Element) ([]byte, error) {
|
||||
rsaKey, err := validateRSAKeyIfPresent(key, ciphertextEl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ciphertext, err := getCiphertext(ciphertextEl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
{
|
||||
digestMethodEl := ciphertextEl.FindElement("./EncryptionMethod/DigestMethod")
|
||||
if digestMethodEl == nil {
|
||||
e.DigestMethod = SHA1
|
||||
} else {
|
||||
hashAlgorithmStr := digestMethodEl.SelectAttrValue("Algorithm", "")
|
||||
digestMethod, ok := digestMethods[hashAlgorithmStr]
|
||||
if !ok {
|
||||
return nil, ErrAlgorithmNotImplemented(hashAlgorithmStr)
|
||||
}
|
||||
e.DigestMethod = digestMethod
|
||||
}
|
||||
}
|
||||
|
||||
return e.keyDecrypter(e, rsaKey, ciphertext)
|
||||
}
|
||||
|
||||
// OAEP returns a version of RSA that implements RSA in OAEP-MGF1P mode. By default
|
||||
// the block cipher used is AES-256 CBC and the digest method is SHA-256. You can
|
||||
// specify other ciphers and digest methods by assigning to BlockCipher or
|
||||
// DigestMethod.
|
||||
func OAEP() RSA {
|
||||
return RSA{
|
||||
BlockCipher: AES256CBC,
|
||||
DigestMethod: SHA256,
|
||||
algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p",
|
||||
keyEncrypter: func(e RSA, pubKey *rsa.PublicKey, plaintext []byte) ([]byte, error) {
|
||||
return rsa.EncryptOAEP(e.DigestMethod.Hash(), RandReader, pubKey, plaintext, nil)
|
||||
},
|
||||
keyDecrypter: func(e RSA, privKey *rsa.PrivateKey, ciphertext []byte) ([]byte, error) {
|
||||
return rsa.DecryptOAEP(e.DigestMethod.Hash(), RandReader, privKey, ciphertext, nil)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// PKCS1v15 returns a version of RSA that implements RSA in PKCS1v15 mode. By default
|
||||
// the block cipher used is AES-256 CBC. The DigestMethod field is ignored because PKCS1v15
|
||||
// does not use a digest function.
|
||||
func PKCS1v15() RSA {
|
||||
return RSA{
|
||||
BlockCipher: AES256CBC,
|
||||
DigestMethod: nil,
|
||||
algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5",
|
||||
keyEncrypter: func(e RSA, pubKey *rsa.PublicKey, plaintext []byte) ([]byte, error) {
|
||||
return rsa.EncryptPKCS1v15(RandReader, pubKey, plaintext)
|
||||
},
|
||||
keyDecrypter: func(e RSA, privKey *rsa.PrivateKey, ciphertext []byte) ([]byte, error) {
|
||||
return rsa.DecryptPKCS1v15(RandReader, privKey, ciphertext)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterDecrypter(OAEP())
|
||||
RegisterDecrypter(PKCS1v15())
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// Package xmlenc is a partial implementation of the xmlenc standard
|
||||
// as described in https://www.w3.org/TR/2002/REC-xmlenc-core-20021210/Overview.html.
|
||||
// The purpose of this implementation is to support encrypted SAML assertions.
|
||||
package xmlenc
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"hash"
|
||||
|
||||
"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 = 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
|
||||
// depending on the implementation.
|
||||
type Encrypter interface {
|
||||
Encrypt(key interface{}, plaintext []byte, nonce []byte) (*etree.Element, error)
|
||||
}
|
||||
|
||||
// Decrypter is an interface that decrypts things. The Decrypt() method returns the
|
||||
// plaintext version of the EncryptedData or EncryptedKey element passed.
|
||||
//
|
||||
// You probably don't have to use this interface directly, instead you may call
|
||||
// Decrypt() and it will examine the element to determine which Decrypter to use.
|
||||
type Decrypter interface {
|
||||
Algorithm() string
|
||||
Decrypt(key interface{}, ciphertextEl *etree.Element) ([]byte, error)
|
||||
}
|
||||
|
||||
// DigestMethod represents a digest method such as SHA1, etc.
|
||||
type DigestMethod interface {
|
||||
Algorithm() string
|
||||
Hash() hash.Hash
|
||||
}
|
||||
|
||||
var (
|
||||
decrypters = map[string]Decrypter{}
|
||||
digestMethods = map[string]DigestMethod{}
|
||||
)
|
||||
|
||||
// RegisterDecrypter registers the specified decrypter to that it can be
|
||||
// used with Decrypt().
|
||||
func RegisterDecrypter(d Decrypter) {
|
||||
decrypters[d.Algorithm()] = d
|
||||
}
|
||||
|
||||
// RegisterDigestMethod registers the specified digest method to that it can be
|
||||
// used with Decrypt().
|
||||
func RegisterDigestMethod(dm DigestMethod) {
|
||||
digestMethods[dm.Algorithm()] = dm
|
||||
}
|
||||
|
||||
// BlockCipher implements a cipher with a fixed size key like AES or 3DES.
|
||||
type BlockCipher interface {
|
||||
Encrypter
|
||||
Decrypter
|
||||
KeySize() int
|
||||
}
|
||||
Reference in New Issue
Block a user