Skip to content

Commit

Permalink
SAML: Parse and Validate AuthnRequest
Browse files Browse the repository at this point in the history
ref DEV-1724
  • Loading branch information
louischan-oursky authored and tung2744 committed Aug 16, 2024
2 parents fdd2fee + acbf9f2 commit 0d2168f
Show file tree
Hide file tree
Showing 20 changed files with 578 additions and 15 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ require (
github.com/authgear/oauthrelyingparty v1.4.0
github.com/beevik/etree v1.1.0
github.com/crewjam/saml v0.4.14
github.com/mattermost/xml-roundtrip-validator v0.1.0
)

require (
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/russellhaering/goxmldsig v1.3.0 // indirect
)

Expand Down
1 change: 1 addition & 0 deletions pkg/auth/handler/saml/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
)

var DependencySet = wire.NewSet(
NewLoginHandlerLogger,
wire.Struct(new(MetadataHandler), "*"),
wire.Struct(new(LoginHandler), "*"),
)
133 changes: 128 additions & 5 deletions pkg/auth/handler/saml/login.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
package saml

import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"time"

"github.com/authgear/authgear-server/pkg/lib/config"
"github.com/authgear/authgear-server/pkg/lib/saml"
"github.com/authgear/authgear-server/pkg/lib/saml/binding"
"github.com/authgear/authgear-server/pkg/lib/saml/protocol"
"github.com/authgear/authgear-server/pkg/util/clock"
"github.com/authgear/authgear-server/pkg/util/httproute"
"github.com/authgear/authgear-server/pkg/util/log"
)

func ConfigureLoginRoute(route httproute.Route) httproute.Route {
Expand All @@ -13,22 +24,134 @@ func ConfigureLoginRoute(route httproute.Route) httproute.Route {
WithPathPattern("/saml2/login/:service_provider_id")
}

type LoginHandlerSAMLService interface {
type LoginHandlerLogger struct{ *log.Logger }

func NewLoginHandlerLogger(lf *log.Factory) *LoginHandlerLogger {
return &LoginHandlerLogger{lf.New("saml-login-handler")}
}

type LoginHandler struct {
Logger *LoginHandlerLogger
Clock clock.Clock
SAMLConfig *config.SAMLConfig
SAMLService MetadataHandlerSAMLService
SAMLService HandlerSAMLService
}

func (h *LoginHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
now := h.Clock.NowUTC()
serviceProviderId := httproute.GetParam(r, "service_provider_id")
_, ok := h.SAMLConfig.ResolveProvider(serviceProviderId)
sp, ok := h.SAMLConfig.ResolveProvider(serviceProviderId)
if !ok {
http.NotFound(rw, r)
return
}

// TODO
_, _ = rw.Write([]byte("ok"))
callbackURL := sp.DefaultAcsURL()

var err error
var relayState string
var authnRequest *saml.AuthnRequest

switch r.Method {
case "GET":
// HTTP-Redirect binding
authnRequest, relayState, err = h.handleRedirectBinding(now, r)
case "POST":
// HTTP-POST binding
authnRequest, relayState, err = h.handlePostBinding(now, r)
default:
panic(fmt.Errorf("unexpected method %s", r.Method))
}

if err != nil {
var protocolErr *protocol.SAMLProtocolError
if errors.As(err, &protocolErr) {
h.handleProtocolError(rw, protocolErr)
return
}
panic(err)
}

err = h.SAMLService.ValidateAuthnRequest(sp.ID, authnRequest)
if err != nil {
protocolErr := &protocol.SAMLProtocolError{
Response: saml.NewRequestDeniedErrorResponse(now, "failed to validate SAMLRequest"),
RelayState: relayState,
Cause: err,
}
h.handleProtocolError(rw, protocolErr)
return
}

if authnRequest.AssertionConsumerServiceURL != "" {
callbackURL = authnRequest.AssertionConsumerServiceURL
}

// TODO(saml): Redirect to auth ui
_, _ = rw.Write([]byte("callback url:" + callbackURL + "\n" + authnRequest.ID + relayState))
}

func (h *LoginHandler) handleRedirectBinding(now time.Time, r *http.Request) (authnRequest *saml.AuthnRequest, relayState string, err error) {
relayState = r.URL.Query().Get("RelayState")
compressedRequest, err := base64.StdEncoding.DecodeString(r.URL.Query().Get("SAMLRequest"))
if err != nil {
return nil, relayState, &protocol.SAMLProtocolError{
Response: saml.NewRequestDeniedErrorResponse(now, "failed to decode SAMLRequest"),
RelayState: relayState,
Cause: err,
}
}
requestBuffer, err := io.ReadAll(binding.NewSaferFlateReader(bytes.NewReader(compressedRequest)))
if err != nil {
return nil, relayState, &protocol.SAMLProtocolError{
Response: saml.NewRequestDeniedErrorResponse(now, "failed to decompress SAMLRequest"),
RelayState: relayState,
Cause: err,
}
}

authnRequest, err = h.SAMLService.ParseAuthnRequest(requestBuffer)
if err != nil {
return nil, relayState, &protocol.SAMLProtocolError{
Response: saml.NewRequestDeniedErrorResponse(now, "failed to parse SAMLRequest"),
RelayState: relayState,
Cause: err,
}
}
return authnRequest, relayState, nil
}

func (h *LoginHandler) handlePostBinding(now time.Time, r *http.Request) (authnRequest *saml.AuthnRequest, relayState string, err error) {
if err := r.ParseForm(); err != nil {
return nil, "", &protocol.SAMLProtocolError{
Response: saml.NewRequestDeniedErrorResponse(now, "failed to parse request body"),
Cause: err,
}
}
relayState = r.PostForm.Get("RelayState")

requestBuffer, err := base64.StdEncoding.DecodeString(r.PostForm.Get("SAMLRequest"))
if err != nil {
return nil, relayState, &protocol.SAMLProtocolError{
Response: saml.NewRequestDeniedErrorResponse(now, "failed to decode SAMLRequest"),
RelayState: relayState,
Cause: err,
}
}

authnRequest, err = h.SAMLService.ParseAuthnRequest(requestBuffer)
if err != nil {
return nil, relayState, &protocol.SAMLProtocolError{
Response: saml.NewRequestDeniedErrorResponse(now, "failed to parse SAMLRequest"),
RelayState: relayState,
Cause: err,
}
}
return authnRequest, relayState, nil
}

func (h *LoginHandler) handleProtocolError(rw http.ResponseWriter, err *protocol.SAMLProtocolError) {
h.Logger.Warnln(err.Error())
// TODO(saml): Return the error to acs url
panic(err)
}
6 changes: 1 addition & 5 deletions pkg/auth/handler/saml/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@ func ConfigureMetadataRoute(route httproute.Route) httproute.Route {
WithPathPattern("/saml2/metadata/:service_provider_id")
}

type MetadataHandlerSAMLService interface {
IdpMetadata(serviceProviderId string) (*saml.Metadata, error)
}

type MetadataHandler struct {
SAMLService MetadataHandlerSAMLService
SAMLService HandlerSAMLService
}

func (h *MetadataHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
Expand Down
9 changes: 9 additions & 0 deletions pkg/auth/handler/saml/services.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package saml

import "github.com/authgear/authgear-server/pkg/lib/saml"

type HandlerSAMLService interface {
IdpMetadata(serviceProviderId string) (*saml.Metadata, error)
ParseAuthnRequest(input []byte) (*saml.AuthnRequest, error)
ValidateAuthnRequest(serviceProviderId string, authnRequest *saml.AuthnRequest) error
}
6 changes: 5 additions & 1 deletion pkg/auth/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions pkg/lib/config/saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ var _ = Schema.Add("SAMLServiceProviderConfig", `
"properties": {
"id": { "type": "string" },
"nameid_format": { "$ref": "#/$defs/SAMLNameIDFormat" },
"nameid_attribute_pointer": { "$ref": "#/$defs/SAMLNameIDAttributePointer" }
"nameid_attribute_pointer": { "$ref": "#/$defs/SAMLNameIDAttributePointer" },
"acs_urls": {
"type": "array",
"items": { "type": "string", "format": "uri" },
"minItems": 1
}
},
"required": ["id"]
"required": ["id", "acs_urls"]
}
`)

Expand Down Expand Up @@ -77,6 +82,7 @@ type SAMLServiceProviderConfig struct {
ID string `json:"id,omitempty"`
NameIDFormat SAMLNameIDFormat `json:"nameid_format,omitempty"`
NameIDAttributePointer SAMLNameIDAttributePointer `json:"nameid_attribute_pointer,omitempty"`
AcsURLs []string `json:"acs_urls,omitempty"`
}

func (c *SAMLServiceProviderConfig) SetDefaults() {
Expand All @@ -89,6 +95,10 @@ func (c *SAMLServiceProviderConfig) SetDefaults() {
}
}

func (c *SAMLServiceProviderConfig) DefaultAcsURL() string {
return c.AcsURLs[0]
}

var _ = Schema.Add("SAMLSigningConfig", `
{
"type": "object",
Expand Down
2 changes: 2 additions & 0 deletions pkg/lib/config/testdata/config_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,8 @@ config:
- id: provider_1
nameid_format: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
nameid_attribute_pointer: /preferred_username
acs_urls:
- http://localhost:3000/saml-test
---
name: saml-signing
error: null
Expand Down
2 changes: 1 addition & 1 deletion pkg/lib/deps/deps_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ var CommonDependencySet = wire.NewSet(
wire.NewSet(
saml.DependencySet,

wire.Bind(new(handlersaml.MetadataHandlerSAMLService), new(*saml.Service)),
wire.Bind(new(handlersaml.HandlerSAMLService), new(*saml.Service)),
),

wire.NewSet(
Expand Down
15 changes: 15 additions & 0 deletions pkg/lib/saml/authn_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package saml

import (
crewjamsaml "github.com/crewjam/saml"

"github.com/authgear/authgear-server/pkg/lib/saml/binding"
)

type AuthnRequest struct {
crewjamsaml.AuthnRequest
}

func (a *AuthnRequest) GetProtocolBinding() binding.SAMLBinding {
return binding.SAMLBinding(a.AuthnRequest.ProtocolBinding)
}
26 changes: 26 additions & 0 deletions pkg/lib/saml/binding/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package binding

import (
crewjamsaml "github.com/crewjam/saml"
)

type SAMLBinding string

const (
SAMLBindingHTTPRedirect SAMLBinding = crewjamsaml.HTTPRedirectBinding
SAMLBindingPostRedirect SAMLBinding = crewjamsaml.HTTPPostBinding
)

var SupportedBindings []SAMLBinding = []SAMLBinding{
SAMLBindingHTTPRedirect,
SAMLBindingPostRedirect,
}

func (b SAMLBinding) IsSupported() bool {
for _, supported := range SupportedBindings {
if b == supported {
return true
}
}
return false
}
33 changes: 33 additions & 0 deletions pkg/lib/saml/binding/flate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copied from https://github.com/crewjam/saml/blob/8e9236867d176ad6338c870a84e2039aef8a5021/flate.go

package binding

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()
}
6 changes: 6 additions & 0 deletions pkg/lib/saml/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package saml

const (
// https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf 3.2.2
SAMLVersion2 string = "2.0"
)
24 changes: 24 additions & 0 deletions pkg/lib/saml/id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package saml

import (
"fmt"

"github.com/authgear/authgear-server/pkg/util/rand"
)

const (
idAlphabet string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)

// It must start with a letter or underscore, and can only contain letters, digits, underscores, hyphens, and periods.
// https://www.w3.org/TR/2012/REC-xmlschema11-2-20120405/datatypes.html#ID
// https://www.w3.org/TR/2012/REC-xmlschema11-2-20120405/datatypes.html#NCName

func generateID(prefix string) string {
id := rand.StringWithAlphabet(32, idAlphabet, rand.SecureRand)
return fmt.Sprintf("%s_%s", prefix, id)
}

func GenerateResponseID() string {
return generateID("samlresponse")
}
File renamed without changes.
Loading

0 comments on commit 0d2168f

Please sign in to comment.