first commit
This commit is contained in:
commit
c4f6ffce92
|
@ -0,0 +1,3 @@
|
|||
*.key
|
||||
*.crt
|
||||
/saml-passthrough
|
|
@ -0,0 +1,21 @@
|
|||
# saml-passthrough
|
||||
This program is intended to run behind the Apache mod_auth_mellon module.
|
||||
It receives ADFS user information from Apache over FastCGI, then
|
||||
passes it back to Keycloak (which is acting as a SAML SP).
|
||||
|
||||
## Create a new keypair
|
||||
```sh
|
||||
openssl req -newkey rsa:2048 -nodes -keyout idp.key -x509 -out idp.crt -days 3680 -subj '/CN=SAML Passthrough/O=Computer Science Club'
|
||||
```
|
||||
Make sure to renew the cert in ten years.
|
||||
|
||||
## Apache config
|
||||
Add the following snippet to /etc/apache2/sites-real/csc (and make sure mod_proxy_fcgi is enabled):
|
||||
```
|
||||
<Location /keycloak/saml/ >
|
||||
AuthType Mellon
|
||||
MellonEnable auth
|
||||
Require valid-user
|
||||
SetHandler "proxy:unix:/run/saml-passthrough/server.sock|fcgi://localhost"
|
||||
</Location>
|
||||
```
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"idp_key_path": "./idp.key",
|
||||
"idp_cert_path": "./idp.crt",
|
||||
"username_key": "MELLON_samaccountname",
|
||||
"email_key": "MELLON_emailaddress",
|
||||
"given_name_key": "MELLON_givenname",
|
||||
"surname_key": "MELLON_http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
|
||||
"groups_key": "ADFS_GROUP",
|
||||
"base_url": "https://csclub.uwaterloo.ca/keycloak/saml",
|
||||
"sp_metadata_paths": [
|
||||
"https://keycloak.csclub.uwaterloo.ca/auth/realms/csc/broker/adfs/endpoint/descriptor"
|
||||
],
|
||||
"socket_path": "/run/saml-passthrough/server.sock"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
module git.csclub.uwaterloo.ca/merenber/saml-passthrough
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/crewjam/saml v0.4.6
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0
|
||||
)
|
|
@ -0,0 +1,64 @@
|
|||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
|
||||
github.com/crewjam/saml v0.4.6 h1:XCUFPkQSJLvzyl4cW9OvpWUbRf0gE7VUpU8ZnilbeM4=
|
||||
github.com/crewjam/saml v0.4.6/go.mod h1:ZBOXnNPFzB3CgOkRm7Nd6IVdkG+l/wF+0ZXLqD96t1A=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0=
|
||||
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/russellhaering/goxmldsig v1.1.1 h1:vI0r2osGF1A9PLvsGdPUAGwEIrKa4Pj5sesSBsebIxM=
|
||||
github.com/russellhaering/goxmldsig v1.1.1/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8=
|
||||
github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
|
@ -0,0 +1,161 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/fcgi"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
"github.com/crewjam/saml/logger"
|
||||
)
|
||||
|
||||
// adapted from https://github.com/crewjam/saml/blob/main/example/idp/idp.go
|
||||
func parsePEM(filename string) *pem.Block {
|
||||
raw, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
block, _ := pem.Decode(raw)
|
||||
if block == nil {
|
||||
panic("Could not decode PEM from " + filename)
|
||||
}
|
||||
return block
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// The filesystem path to the private key to use for the IDP.
|
||||
// The key should be in PKCS8 PEM format.
|
||||
IDPKeyPath string `json:"idp_key_path"`
|
||||
// The filesystem path to the certificate to use for the IDP.
|
||||
// The certificate should be in PEM format.
|
||||
IDPCertPath string `json:"idp_cert_path"`
|
||||
// The name of the FCGI variable which has the user's username.
|
||||
UsernameKey string `json:"username_key"`
|
||||
// The name of the FCGI variable which has the user's email.
|
||||
EmailKey string `json:"email_key"`
|
||||
// The name of the FCGI variable which has the user's first name.
|
||||
GivenNameKey string `json:"given_name_key"`
|
||||
// The name of the FCGI variable which has the user's last name.
|
||||
SurnameKey string `json:"surname_key"`
|
||||
// The name of the FCGI variable which has the user's groups.
|
||||
// The groups should be concatenated together into one string
|
||||
// separated by semicolons, e.g. "group1;group2".
|
||||
GroupsKey string `json:"groups_key"`
|
||||
// The base URL for the IDP. The following endpoints will be
|
||||
// derived from it:
|
||||
// Metadata: baseURL + "/metadata"
|
||||
// SSO login: baseURL + "/sso"
|
||||
BaseURL string `json:"base_url"`
|
||||
// An array of filepaths or HTTP URLs of the service providers'
|
||||
// metadata XML files.
|
||||
SPMetadataPaths []string `json:"sp_metadata_paths"`
|
||||
// The filesystem path of the Unix domain socket on which this
|
||||
// program will listen for incoming FCGI connections.
|
||||
SocketPath string `json:"socket_path"`
|
||||
}
|
||||
|
||||
func newIDP(cfg *Config) *saml.IdentityProvider {
|
||||
key := func() crypto.PrivateKey {
|
||||
block := parsePEM(cfg.IDPKeyPath)
|
||||
k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return k
|
||||
}()
|
||||
cert := func() *x509.Certificate {
|
||||
block := parsePEM(cfg.IDPCertPath)
|
||||
c, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return c
|
||||
}()
|
||||
sessionProvider := &BasicSessionProvider{
|
||||
UsernameKey: cfg.UsernameKey,
|
||||
EmailKey: cfg.EmailKey,
|
||||
GivenNameKey: cfg.GivenNameKey,
|
||||
SurnameKey: cfg.SurnameKey,
|
||||
GroupsKey: cfg.GroupsKey,
|
||||
}
|
||||
serviceProviderProvider := NewServiceProviderProvider(cfg.SPMetadataPaths)
|
||||
metadataURL, err := url.Parse(cfg.BaseURL + "/metadata")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ssoURL, err := url.Parse(cfg.BaseURL + "/sso")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &saml.IdentityProvider{
|
||||
Key: key,
|
||||
Certificate: cert,
|
||||
Logger: logger.DefaultLogger,
|
||||
MetadataURL: *metadataURL,
|
||||
SSOURL: *ssoURL,
|
||||
SessionProvider: sessionProvider,
|
||||
ServiceProviderProvider: serviceProviderProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func newHTTPHandler(idp *saml.IdentityProvider) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc(idp.MetadataURL.Path, idp.ServeMetadata)
|
||||
mux.HandleFunc(idp.SSOURL.Path, idp.ServeSSO)
|
||||
return mux
|
||||
}
|
||||
|
||||
func newListener(cfg *Config) net.Listener {
|
||||
var err error
|
||||
sockPath := cfg.SocketPath
|
||||
if _, err = os.Stat(sockPath); err == nil {
|
||||
err = os.Remove(sockPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
ln, err := net.Listen("unix", sockPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
os.Chmod(sockPath, 0777)
|
||||
return ln
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Allow the key path to be overriden via a CLI flag so that we
|
||||
// can use systemd's LoadCredential feature
|
||||
idpKeyPathPtr := flag.String("k", "", "IDP key path")
|
||||
configPathPtr := flag.String("c", "./config.json", "config file path")
|
||||
flag.Parse()
|
||||
|
||||
cfgBytes, err := ioutil.ReadFile(*configPathPtr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cfg := Config{}
|
||||
err = json.Unmarshal(cfgBytes, &cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if *idpKeyPathPtr != "" {
|
||||
cfg.IDPKeyPath = *idpKeyPathPtr
|
||||
}
|
||||
|
||||
idp := newIDP(&cfg)
|
||||
handler := newHTTPHandler(idp)
|
||||
listener := newListener(&cfg)
|
||||
|
||||
err = fcgi.Serve(listener, handler)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
// adapted from https://github.com/crewjam/saml/blob/main/samlidp/service.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
xrv "github.com/mattermost/xml-roundtrip-validator"
|
||||
)
|
||||
|
||||
// copied from https://github.com/crewjam/saml/blob/main/samlidp/util.go
|
||||
func getSPMetadata(r io.Reader) (spMetadata *saml.EntityDescriptor, err error) {
|
||||
var data []byte
|
||||
if data, err = ioutil.ReadAll(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spMetadata = &saml.EntityDescriptor{}
|
||||
if err := xrv.Validate(bytes.NewBuffer(data)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := xml.Unmarshal(data, &spMetadata); err != nil {
|
||||
if err.Error() == "expected element type <EntityDescriptor> but have <EntitiesDescriptor>" {
|
||||
entities := &saml.EntitiesDescriptor{}
|
||||
if err := xml.Unmarshal(data, &entities); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, e := range entities.EntityDescriptors {
|
||||
if len(e.SPSSODescriptors) > 0 {
|
||||
return &e, nil
|
||||
}
|
||||
}
|
||||
|
||||
// there were no SPSSODescriptors in the response
|
||||
return nil, errors.New("metadata contained no service provider metadata")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return spMetadata, nil
|
||||
}
|
||||
|
||||
type BasicServiceProviderProvider struct {
|
||||
serviceProviders map[string]*saml.EntityDescriptor
|
||||
}
|
||||
|
||||
func modifyACSBindings(entityDescriptor *saml.EntityDescriptor) {
|
||||
// The SAML library only allows the HTTP-POST Binding (from the IdP
|
||||
// to the SP), so we need to modify the AssertionConsumerService
|
||||
// endpoints which use HTTP-Redirect to use HTTP-POST.
|
||||
for i := 0; i < len(entityDescriptor.SPSSODescriptors); i++ {
|
||||
spSSODescriptor := &entityDescriptor.SPSSODescriptors[i]
|
||||
for j := 0; j < len(spSSODescriptor.AssertionConsumerServices); j++ {
|
||||
acs := &spSSODescriptor.AssertionConsumerServices[j]
|
||||
if acs.Binding == "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" {
|
||||
log.Printf("Replacing Binding for %s from HTTP-Redirect to HTTP-POST", acs.Location)
|
||||
acs.Binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewServiceProviderProvider(spMetadataPaths []string) saml.ServiceProviderProvider {
|
||||
spp := &BasicServiceProviderProvider{
|
||||
serviceProviders: make(map[string]*saml.EntityDescriptor),
|
||||
}
|
||||
client := http.Client{}
|
||||
loadSPMetadata := func(filename string) (*saml.EntityDescriptor, error) {
|
||||
var r io.ReadCloser
|
||||
var err error
|
||||
if strings.HasPrefix(filename, "http://") || strings.HasPrefix(filename, "https://") {
|
||||
resp, err := client.Get(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r = resp.Body
|
||||
} else {
|
||||
r, err = os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
defer r.Close()
|
||||
return getSPMetadata(r)
|
||||
}
|
||||
for _, filename := range spMetadataPaths {
|
||||
metadata, err := loadSPMetadata(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
modifyACSBindings(metadata)
|
||||
spp.serviceProviders[metadata.EntityID] = metadata
|
||||
}
|
||||
return spp
|
||||
}
|
||||
|
||||
// GetServiceProvider returns the Service Provider metadata for the
|
||||
// service provider ID, which is typically the service provider's
|
||||
// metadata URL. If an appropriate service provider cannot be found then
|
||||
// the returned error must be os.ErrNotExist.
|
||||
func (s *BasicServiceProviderProvider) GetServiceProvider(r *http.Request, serviceProviderID string) (*saml.EntityDescriptor, error) {
|
||||
rv, ok := s.serviceProviders[serviceProviderID]
|
||||
if !ok {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return rv, nil
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
// adapted from https://github.com/crewjam/saml/blob/main/samlidp/session.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/fcgi"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
)
|
||||
|
||||
var sessionMaxAge = time.Hour
|
||||
|
||||
func randomBytes(n int) []byte {
|
||||
rv := make([]byte, n)
|
||||
if _, err := saml.RandReader.Read(rv); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Email string
|
||||
GivenName string
|
||||
Surname string
|
||||
Groups []string
|
||||
}
|
||||
|
||||
type BasicSessionProvider struct {
|
||||
UsernameKey string
|
||||
EmailKey string
|
||||
GivenNameKey string
|
||||
SurnameKey string
|
||||
GroupsKey string
|
||||
}
|
||||
|
||||
func (s *BasicSessionProvider) getUser(r *http.Request) *User {
|
||||
env := fcgi.ProcessEnv(r)
|
||||
user := &User{}
|
||||
user.Username, _ = env[s.UsernameKey]
|
||||
user.Email, _ = env[s.EmailKey]
|
||||
user.GivenName, _ = env[s.GivenNameKey]
|
||||
user.Surname, _ = env[s.SurnameKey]
|
||||
groupsStr, _ := env[s.GroupsKey]
|
||||
user.Groups = strings.Split(groupsStr, ";")
|
||||
return user
|
||||
}
|
||||
|
||||
func (s *BasicSessionProvider) GetSession(w http.ResponseWriter, r *http.Request, req *saml.IdpAuthnRequest) *saml.Session {
|
||||
user := s.getUser(r)
|
||||
if user.Username == "" {
|
||||
log.Println("Could not obtain username from FCGI")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
|
||||
session := &saml.Session{
|
||||
ID: base64.StdEncoding.EncodeToString(randomBytes(32)),
|
||||
NameID: user.Username,
|
||||
CreateTime: saml.TimeNow(),
|
||||
ExpireTime: saml.TimeNow().Add(sessionMaxAge),
|
||||
Index: hex.EncodeToString(randomBytes(32)),
|
||||
UserName: user.Username,
|
||||
Groups: user.Groups,
|
||||
UserEmail: user.Email,
|
||||
UserSurname: user.Surname,
|
||||
UserGivenName: user.GivenName,
|
||||
}
|
||||
return session
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
[Unit]
|
||||
Description=SAML passthrough for Keycloak
|
||||
Documentation=https://git.csclub.uwaterloo.ca/merenber/saml-passthrough
|
||||
Requires=apache2.service
|
||||
After=apache2.service
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
WorkingDirectory=/srv/saml-passthrough
|
||||
RuntimeDirectory=saml-passthrough
|
||||
DynamicUser=yes
|
||||
LoadCredential=idp.key:/srv/saml-passthrough/idp.key
|
||||
ExecStart=/srv/saml-passthrough/saml-passthrough -k "${CREDENTIALS_DIRECTORY}/idp.key"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
Loading…
Reference in New Issue