Skip to content

Commit 48a1e2f

Browse files
Clean up documentation and presentation for v2
- Rewrite README with SP/IdP quick starts, security defaults table, tested IdPs list, and links to migration guide and GoDoc - Add MIGRATING.md covering all breaking changes from v1 - Add package-level doc.go for root, sp/, idp/, and types/ packages - Add GoDoc comments to all undocumented exported functions/types - Rewrite SP example: replace panic with log.Fatal, ioutil with io, add RequestTracker and ClockSkew configuration - Add IdP example showing AuthnRequest handling and response building - Expand SECURITY.md with reporting process, design principles, test coverage categories, and known limitations - Rename s2example/ to examples/sp/ and examples/idp/ - Remove V2_PLAN.md (internal planning doc)
1 parent 97fee93 commit 48a1e2f

25 files changed

Lines changed: 815 additions & 444 deletions

MIGRATING.md

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Migrating from gosaml2 v1 to v2
2+
3+
This guide covers the breaking changes in gosaml2 v2 and how to update your code.
4+
5+
## Module Path
6+
7+
```
8+
// v1
9+
import "github.com/russellhaering/gosaml2"
10+
11+
// v2
12+
import "github.com/russellhaering/gosaml2/v2"
13+
```
14+
15+
Both versions can coexist in the same project during migration.
16+
17+
## Package Restructure
18+
19+
The SP logic moved from the root package into `sp/`, and a new `idp/` package was added:
20+
21+
```
22+
// v1 - everything in the root package
23+
sp := &saml2.SAMLServiceProvider{...}
24+
25+
// v2 - SP in sp/, shared types in root
26+
import (
27+
saml2 "github.com/russellhaering/gosaml2/v2"
28+
"github.com/russellhaering/gosaml2/v2/sp"
29+
"github.com/russellhaering/gosaml2/v2/types"
30+
)
31+
s := &sp.ServiceProvider{...}
32+
```
33+
34+
## Struct Rename: SAMLServiceProvider → sp.ServiceProvider
35+
36+
The main SP struct was renamed and its fields simplified:
37+
38+
| v1 Field | v2 Field | Notes |
39+
|----------|----------|-------|
40+
| `ServiceProviderIssuer` | `EntityID` | |
41+
| `IdentityProviderIssuer` | `IDPEntityID` | |
42+
| `IdentityProviderSSOURL` | `IDPSSOURL` | |
43+
| `IdentityProviderSSOBinding` | `IDPSSOBinding` | |
44+
| `IdentityProviderSLOURL` | `IDPSLOURL` | |
45+
| `IdentityProviderSLOBinding` | `IDPSLOBinding` | |
46+
| `AssertionConsumerServiceURL` | `ACSURL` | |
47+
| `ServiceProviderSLOURL` | `SLOURL` | |
48+
| `SPKeyStore` | `SPKeyStore` | Type changed to `*saml2.KeyStore` |
49+
| `SPSigningKeyStore` | `SPSigningKeyStore` | Type changed to `*saml2.KeyStore` |
50+
| `IDPCertificateStore` | `IDPCertificates` | Now `[]*x509.Certificate` directly |
51+
| `SkipSignatureValidation` | `InsecureSkipSignatureValidation` | Renamed to discourage production use |
52+
| `AudienceURI` | `AudienceURIs` | Now a `[]string` for multiple audiences |
53+
54+
## WarningInfo Removal
55+
56+
In v1, condition violations (expired assertions, audience mismatch) were returned as warnings in `WarningInfo`. Callers had to remember to check them.
57+
58+
In v2, these are hard errors. There is no `WarningInfo` struct.
59+
60+
```go
61+
// v1 - easy to forget the warning check
62+
info, err := sp.RetrieveAssertionInfo(encoded)
63+
if err != nil {
64+
// handle error
65+
}
66+
if info.WarningInfo.NotInAudience {
67+
// easy to forget this!
68+
}
69+
70+
// v2 - conditions are errors, no warnings to forget
71+
info, err := sp.RetrieveAssertionInfo(ctx, encoded)
72+
if err != nil {
73+
// audience mismatch, expiry, etc. are all errors now
74+
}
75+
```
76+
77+
The `OneTimeUse` and `ProxyRestriction` conditions are informational fields on `AssertionInfo` instead.
78+
79+
## Error Handling: Sentinel Errors + errors.Is()
80+
81+
v2 uses sentinel errors for programmatic error matching:
82+
83+
```go
84+
import (
85+
"errors"
86+
saml2 "github.com/russellhaering/gosaml2/v2"
87+
)
88+
89+
info, err := s.RetrieveAssertionInfo(ctx, encoded)
90+
if errors.Is(err, saml2.ErrExpired) {
91+
// assertion expired
92+
} else if errors.Is(err, saml2.ErrAudienceMismatch) {
93+
// wrong audience
94+
} else if errors.Is(err, saml2.ErrReplay) {
95+
// InResponseTo not recognized
96+
} else if err != nil {
97+
// other error
98+
}
99+
```
100+
101+
Available sentinel errors: `ErrExpired`, `ErrNotYetValid`, `ErrAudienceMismatch`, `ErrBadRecipient`, `ErrBadDestination`, `ErrBadIssuer`, `ErrBadSignature`, `ErrMissingSignature`, `ErrBadStatus`, `ErrReplay`, `ErrMissingAssertion`, `ErrMissingElement`, `ErrBadVersion`, `ErrMalformed`.
102+
103+
## context.Context on Public Methods
104+
105+
All validation and request-building methods now take `context.Context` as the first parameter:
106+
107+
```go
108+
// v1
109+
info, err := sp.RetrieveAssertionInfo(encoded)
110+
111+
// v2
112+
info, err := s.RetrieveAssertionInfo(ctx, encoded)
113+
```
114+
115+
## RequestTracker for Replay Prevention
116+
117+
v2 adds an optional `RequestTracker` interface for `InResponseTo` validation. When set, the SP validates that each response corresponds to a request it previously sent:
118+
119+
```go
120+
s := &sp.ServiceProvider{
121+
// ...
122+
RequestTracker: sp.NewMemoryRequestTracker(5 * time.Minute),
123+
}
124+
```
125+
126+
When `RequestTracker` is set and `AllowIDPInitiated` is false (the default), responses without `InResponseTo` are rejected.
127+
128+
For single-instance deployments, use `sp.NewMemoryRequestTracker`. For multi-instance deployments, implement the `RequestTracker` interface against your session store (Redis, database, etc.).
129+
130+
## Security Defaults That Changed
131+
132+
| Behavior | v1 | v2 |
133+
|----------|----|----|
134+
| SHA-1 signatures | Accepted | Rejected (set `AllowSHA1: true` to allow) |
135+
| Unsigned logout requests | Silently accepted | Rejected |
136+
| Condition violations | Warnings | Hard errors |
137+
| `SkipSignatureValidation` | Named simply | Renamed to `InsecureSkipSignatureValidation` |
138+
| Assertion version check | Not checked | Must be "2.0" |
139+
| `SubjectConfirmationData.NotBefore` | Not checked | Validated with clock skew |
140+
| `LogoutRequest.NotOnOrAfter` | Not checked | Validated with clock skew |
141+
142+
## Metadata Configuration
143+
144+
v2 provides helpers to configure SP or IdP from partner metadata:
145+
146+
```go
147+
// SP: configure from IdP metadata
148+
ed, err := saml2.ParseEntityDescriptor(idpMetadataXML)
149+
if err != nil {
150+
log.Fatal(err)
151+
}
152+
if err := s.ConfigureFromMetadata(ed); err != nil {
153+
log.Fatal(err)
154+
}
155+
156+
// IdP: configure SP from its metadata
157+
ed, err := saml2.ParseEntityDescriptor(spMetadataXML)
158+
if err != nil {
159+
log.Fatal(err)
160+
}
161+
spConfig, err := idp.ConfigureFromSPMetadata(ed)
162+
if err != nil {
163+
log.Fatal(err)
164+
}
165+
```
166+
167+
## Clock Skew
168+
169+
v2 adds a configurable `ClockSkew` field (default: 60 seconds) applied to all time comparisons:
170+
171+
```go
172+
s := &sp.ServiceProvider{
173+
// ...
174+
ClockSkew: 30 * time.Second,
175+
}
176+
```
177+
178+
## 3DES Removal
179+
180+
TripleDES (3DES-CBC) encryption is no longer supported. If your IdP uses 3DES, it must be configured to use AES instead.

README.md

Lines changed: 155 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,171 @@
11
# gosaml2
22

33
[![Build Status](https://github.com/russellhaering/gosaml2/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/russellhaering/gosaml2/actions/workflows/test.yml?query=branch%3Amain)
4-
[![GoDoc](https://godoc.org/github.com/russellhaering/gosaml2?status.svg)](https://godoc.org/github.com/russellhaering/gosaml2)
4+
[![GoDoc](https://pkg.go.dev/badge/github.com/russellhaering/gosaml2/v2)](https://pkg.go.dev/github.com/russellhaering/gosaml2/v2)
55

6-
SAML 2.0 implemementation for Service Providers based on [etree](https://github.com/beevik/etree)
7-
and [goxmldsig](https://github.com/russellhaering/goxmldsig), a pure Go
8-
implementation of XML digital signatures.
6+
SAML 2.0 library for Go with both **Service Provider** and **Identity Provider** support. Built on [etree](https://github.com/beevik/etree) and a vendored pure-Go XML digital signatures implementation.
7+
8+
## Features
9+
10+
- **Service Provider (SP)**: Validate SAML responses, build AuthnRequests, generate SP metadata, single logout
11+
- **Identity Provider (IdP)**: Build SAML responses, validate AuthnRequests, generate IdP metadata, single logout
12+
- **Secure defaults**: SHA-1 disabled, signatures required, IDP-initiated SSO off by default
13+
- **Replay prevention**: `RequestTracker` interface with in-memory implementation for `InResponseTo` validation
14+
- **Encryption**: AES-GCM and AES-CBC assertion encryption/decryption
15+
- **HTTP bindings**: HTTP-POST and HTTP-Redirect with proper query-string signatures
16+
- **Metadata parsing**: `ParseEntityDescriptor` for configuring SP or IdP from partner metadata XML
17+
- **Comprehensive test suite**: 335 security tests covering signature validation, replay attacks, XML wrapping, and more
918

1019
## Installation
1120

12-
Install `gosaml2` into your `$GOPATH` using `go get`:
21+
```
22+
go get github.com/russellhaering/gosaml2/v2
23+
```
24+
25+
Requires Go 1.23 or later.
26+
27+
## Quick Start: Service Provider
28+
29+
```go
30+
package main
1331

32+
import (
33+
"context"
34+
"fmt"
35+
"log"
36+
"net/http"
37+
"time"
38+
39+
saml2 "github.com/russellhaering/gosaml2/v2"
40+
"github.com/russellhaering/gosaml2/v2/sp"
41+
"github.com/russellhaering/gosaml2/v2/types"
42+
)
43+
44+
func main() {
45+
// Parse your IdP's metadata XML to extract SSO URL and certificates.
46+
ed, err := saml2.ParseEntityDescriptor(idpMetadataXML)
47+
if err != nil {
48+
log.Fatal(err)
49+
}
50+
51+
s := &sp.ServiceProvider{
52+
EntityID: "https://my-app.example.com",
53+
ACSURL: "https://my-app.example.com/saml/acs",
54+
AudienceURIs: []string{"https://my-app.example.com"},
55+
// Replay prevention (recommended).
56+
RequestTracker: sp.NewMemoryRequestTracker(5 * time.Minute),
57+
}
58+
// Configure IdP settings from metadata.
59+
if err := s.ConfigureFromMetadata(ed); err != nil {
60+
log.Fatal(err)
61+
}
62+
63+
http.HandleFunc("/saml/acs", func(w http.ResponseWriter, r *http.Request) {
64+
r.ParseForm()
65+
info, err := s.RetrieveAssertionInfo(context.Background(), r.FormValue("SAMLResponse"))
66+
if err != nil {
67+
http.Error(w, "Forbidden", http.StatusForbidden)
68+
return
69+
}
70+
fmt.Fprintf(w, "Hello, %s\n", info.NameID)
71+
})
72+
}
1473
```
15-
go get github.com/russellhaering/gosaml2
74+
75+
## Quick Start: Identity Provider
76+
77+
```go
78+
package main
79+
80+
import (
81+
"context"
82+
"log"
83+
"net/http"
84+
85+
"github.com/russellhaering/gosaml2/v2/idp"
86+
"github.com/russellhaering/gosaml2/v2/types"
87+
)
88+
89+
func main() {
90+
identity := &idp.IdentityProvider{
91+
EntityID: "https://idp.example.com",
92+
SSOURL: "https://idp.example.com/sso",
93+
SigningKeyStore: signingKey, // *saml2.KeyStore
94+
SignResponses: true,
95+
SignAssertions: true,
96+
ServiceProviders: map[string]*idp.SPConfig{
97+
"https://sp.example.com": {
98+
EntityID: "https://sp.example.com",
99+
ACSURLs: []string{"https://sp.example.com/saml/acs"},
100+
},
101+
},
102+
}
103+
104+
http.HandleFunc("/sso", func(w http.ResponseWriter, r *http.Request) {
105+
reqInfo, err := identity.ValidateEncodedAuthnRequestPOST(context.Background(), r.FormValue("SAMLRequest"))
106+
if err != nil {
107+
http.Error(w, "Bad Request", http.StatusBadRequest)
108+
return
109+
}
110+
// Authenticate user, then build response...
111+
body, err := identity.BuildResponseBodyPost(reqInfo.SP.EntityID, &idp.AssertionParams{
112+
NameID: "user@example.com",
113+
InResponseTo: reqInfo.ID,
114+
Recipient: reqInfo.ACSURL,
115+
}, r.FormValue("RelayState"))
116+
if err != nil {
117+
http.Error(w, "Internal Error", http.StatusInternalServerError)
118+
return
119+
}
120+
w.Write(body)
121+
})
122+
}
16123
```
17124

18-
## Example
125+
## Security Defaults
126+
127+
gosaml2 v2 is secure by default:
128+
129+
| Setting | Default | Override |
130+
|---------|---------|----------|
131+
| SHA-1 signatures | **Rejected** | `AllowSHA1: true` |
132+
| Response/assertion signatures | **Required** | `InsecureSkipSignatureValidation: true` |
133+
| IDP-initiated SSO | **Rejected** | `AllowIDPInitiated: true` |
134+
| Unsigned logout requests | **Rejected** | `InsecureSkipSignatureValidation: true` |
135+
| Conditions (NotBefore/NotOnOrAfter) | **Hard errors** | Not overridable |
136+
| Audience restriction | **Enforced** | Configure `AudienceURIs` |
137+
| Clock skew tolerance | **60 seconds** | `ClockSkew: duration` |
138+
139+
## Examples
140+
141+
- [SP example](examples/sp/demo.go) - Service Provider with Okta
142+
- [IdP example](examples/idp/demo.go) - Identity Provider serving SAML responses
143+
144+
## Migration from v1
145+
146+
See [MIGRATING.md](MIGRATING.md) for a detailed guide on upgrading from gosaml2 v1.
147+
148+
## Tested Identity Providers
149+
150+
This library is meant to be a standards-compliant SAML implementation. The following identity providers have been tested:
151+
152+
- Okta
153+
- Auth0
154+
- Shibboleth
155+
- OneLogin
156+
- Azure Active Directory (Azure AD)
157+
- Keycloak
158+
- Google Workspace
159+
- Microsoft ADFS
19160

20-
See [demo.go](s2example/demo.go).
161+
If you find a standards-compliant identity provider that doesn't work, please submit a bug or pull request.
21162

22-
## Supported Identity Providers
163+
## Documentation
23164

24-
This library is meant to be a generic SAML implementation. If you find a
25-
standards compliant identity provider that it doesn't work with please
26-
submit a bug or pull request.
165+
- [GoDoc (pkg.go.dev)](https://pkg.go.dev/github.com/russellhaering/gosaml2/v2)
166+
- [Migration Guide](MIGRATING.md)
167+
- [Security Policy](SECURITY.md)
27168

28-
The following identity providers have been tested:
169+
## License
29170

30-
* Okta
31-
* Auth0
32-
* Shibboleth
33-
* Ipsilon
34-
* OneLogin
35-
* Azure Active Directory (Azure AD)
171+
Apache License 2.0 - see [LICENSE](LICENSE) for details.

0 commit comments

Comments
 (0)