Skip to content

Commit

Permalink
adding initial saml support
Browse files Browse the repository at this point in the history
  • Loading branch information
matt-deboer committed Aug 9, 2017
1 parent 8cc9402 commit 9fbe9f5
Show file tree
Hide file tree
Showing 423 changed files with 88,350 additions and 14 deletions.
32 changes: 31 additions & 1 deletion Gopkg.lock

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

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@

[[constraint]]
name = "golang.org/x/oauth2"

[[constraint]]
name = "github.com/crewjam/saml"
branch = "master"
6 changes: 5 additions & 1 deletion pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,13 @@ func (m *Manager) respondWithUserInfo(session *SessionToken, w http.ResponseWrit

func (m *Manager) keepSessionAlive(session *SessionToken, w http.ResponseWriter) {
// If the session expires in less than 1 minute, renew it
if session != nil && session.Valid && session.Expires() < (time.Now().Unix()-int64(time.Minute.Seconds())) {
expiration := session.Expires()
if session != nil && session.Valid && expiration < (time.Now().Unix()-int64(time.Minute.Seconds())) {
session = NewSessionToken(session.User(), []string{}, session.claims)
m.writeSessionCookie(session, w)
if log.GetLevel() >= log.DebugLevel {
log.Debugf("Renewed session for %s: expires %d -> %d", session.User(), session.Expires())
}
}
}

Expand Down
6 changes: 2 additions & 4 deletions pkg/auth/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import (

"encoding/base64"

log "github.com/sirupsen/logrus"
oidc "github.com/coreos/go-oidc"
jwt "github.com/dgrijalva/jwt-go"
uuid "github.com/nu7hatch/gouuid"
log "github.com/sirupsen/logrus"
)

const (
Expand All @@ -36,11 +36,10 @@ type oidcHandler struct {
groupsClaim string
idClaim string
iconURL string
authManager *Manager
}

// NewOIDCHandler creates a new oidc handler with the provided configuration items
func NewOIDCHandler(authManager *Manager, name, publicURL, oidcProvider, clientID, clientSecret string, additionalScopes []string, idClaim string, groupsClaim string) (Authenticator, error) {
func NewOIDCHandler(name, publicURL, oidcProvider, clientID, clientSecret string, additionalScopes []string, idClaim string, groupsClaim string) (Authenticator, error) {
if len(name) == 0 {
return nil, fmt.Errorf("'name' is required")
}
Expand All @@ -55,7 +54,6 @@ func NewOIDCHandler(authManager *Manager, name, publicURL, oidcProvider, clientI
name: name,
groupsClaim: groupsClaim,
idClaim: idClaim,
authManager: authManager,
iconURL: iconURL.String(),
}

Expand Down
202 changes: 201 additions & 1 deletion pkg/auth/saml.go
Original file line number Diff line number Diff line change
@@ -1 +1,201 @@
package auth
package auth

import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"encoding/xml"
"fmt"
"net/http"
"net/url"
"path"

"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
log "github.com/sirupsen/logrus"
)

type samlHandler struct {
name string
nonce string
samlSP *samlsp.Middleware
idpMetadata *metadataSummary
groupsAttribute string
groupsDelimiter string
}

type metadataSummary struct {
ssoLoginURL string
signingCerts []*x509.Certificate
issuerID string
}

func NewSamlHandler(publicURL, privateKeyFile, certFile, idpShortName, IDPMetadataURL, groupsAttribute, groupsDelimiter string) (Authenticator, error) {

pu, err := url.Parse(publicURL)
if err != nil {
return nil, fmt.Errorf("Failed to parse public url '%s'; %v", publicURL, err)
}

idpu, err := url.Parse(IDPMetadataURL)
if err != nil {
return nil, fmt.Errorf("Failed to parse idp-metadata-url '%s'; %v", IDPMetadataURL, err)
}

keyPair, err := tls.LoadX509KeyPair(certFile, privateKeyFile)
if err != nil {
return nil, fmt.Errorf("Failed to load keypair cert='%s', key='%s'; %v", certFile, privateKeyFile, err)
}
keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
if err != nil {
return nil, fmt.Errorf("Failed to parse certificate for cert='%s', key='%s'; %v", certFile, privateKeyFile, err)
}

idpMetadata, err := getIDPMetadata(IDPMetadataURL)
if err != nil {
return nil, err
}

s := &samlHandler{
name: idpShortName,
idpMetadata: idpMetadata,
}

s.samlSP, err = samlsp.New(samlsp.Options{

URL: *pu,
Key: keyPair.PrivateKey.(*rsa.PrivateKey),
Certificate: keyPair.Leaf,
IDPMetadataURL: idpu,
AllowIDPInitiated: true,
})
http.HandleFunc(path.Join(s.LoginURL(), "metadata"), s.Metadata)

if err != nil {
return nil, fmt.Errorf("Failed to parse idp-metadata-url '%s'; %v", IDPMetadataURL, err)
}

return s, nil
}

func getIDPMetadata(metadataURL string) (*metadataSummary, error) {
resp, err := http.Get(metadataURL)
if err != nil {
return nil, fmt.Errorf("Failed to fetch saml metadata from '%s'; %v", metadataURL, err)
} else if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Failed to fetch saml metadata from '%s'; %v", metadataURL, resp.StatusCode)
}
var metadata saml.EntitiesDescriptor
err = xml.NewDecoder(resp.Body).Decode(&metadata)
if err != nil {
return nil, fmt.Errorf("Failed to decode entity descriptor: %v", err)
}

summary := &metadataSummary{
issuerID: metadata.EntityDescriptors[0].EntityID,
}

for _, idpSSODescriptor := range metadata.EntityDescriptors[0].IDPSSODescriptors {
// Extract the signing key(s)
summary.signingCerts = append(summary.signingCerts, extractKeys(idpSSODescriptor)...)
// Extract the SSO login endpoint
if len(idpSSODescriptor.SingleSignOnServices) > 0 {
summary.ssoLoginURL = idpSSODescriptor.SingleSignOnServices[0].Location
} else {
return nil, fmt.Errorf("Metadata contains no SSO descriptors")
}
}

return summary, nil
}

func extractKeys(d saml.IDPSSODescriptor) []*x509.Certificate {
certs := []*x509.Certificate{}
for _, keyDesc := range d.KeyDescriptors {
if keyDesc.Use == "signing" || keyDesc.Use == "" {

pemBytes := []byte("-----BEGIN RSA PRIVATE KEY-----\n" +
string(keyDesc.KeyInfo.Certificate) +
"\n-----END CERTIFICATE-----")

pemBlock, rest := pem.Decode(pemBytes)
if rest != nil {
log.Errorf("Failed to decode signing cert from pem bytes %v", string(pemBytes))
} else {
cert, err := x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
log.Errorf("Failed to parse signing certificate; %v", err)
} else {
certs = append(certs, cert)
}
}
}
}
return certs
}

// Name returns the name of this authenticator
func (s *samlHandler) Name() string {
return s.name
}

// Description returns the user-friendly description of this authenticator
func (s *samlHandler) Description() string {
return s.name
}

// Type returns the type of this authenticator
func (s *samlHandler) Type() string {
return "saml"
}

// LoginURL returns the initial login URL for this handler
func (s *samlHandler) LoginURL() string {
return path.Join("/", "auth", s.Type(), s.Name())
}

// PostWithCredentials returns true if this authenticator expects username/password credentials be POST'd
func (s *samlHandler) PostWithCredentials() bool {
return true
}

// IconURL returns an icon URL to signify this login method; empty string implies a default can be used
func (s *samlHandler) IconURL() string {
return ""
}

// Metadata returns the metadata for this service provider
func (s *samlHandler) Metadata(w http.ResponseWriter, r *http.Request) {
buf, _ := xml.MarshalIndent(s.samlSP.ServiceProvider.Metadata(), "", " ")
w.Header().Set("Content-Type", "application/samlmetadata+xml")
w.Write(buf)
}

func (s *samlHandler) Authenticate(w http.ResponseWriter, r *http.Request) (*SessionToken, error) {

if r.Method == "GET" {
http.Redirect(w, r, s.idpMetadata.ssoLoginURL, http.StatusTemporaryRedirect)
} else if r.Method == "POST" {

r.ParseForm()
assertion, err := s.samlSP.ServiceProvider.ParseResponse(r, []string{""})
if err != nil {
if parseErr, ok := err.(*saml.InvalidResponseError); ok {
log.Warnf("RESPONSE: ===\n%s\n===\nNOW: %s\nERROR: %s",
parseErr.Response, parseErr.Now, parseErr.PrivateErr)
}
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return nil, nil
}
return s.sessionTokenFromAssertion(assertion)

} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
return nil, nil
}

func (s *samlHandler) sessionTokenFromAssertion(assertion *saml.Assertion) (*SessionToken, error) {
return nil, nil
}
8 changes: 4 additions & 4 deletions pkg/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (

"net/http/httputil"

log "github.com/sirupsen/logrus"
"github.com/gorilla/websocket"
"github.com/matt-deboer/kapow/pkg/auth"
log "github.com/sirupsen/logrus"
)

var whitelistedHeaders = []string{"Content-Type"}
Expand Down Expand Up @@ -89,15 +89,15 @@ func NewKubeAPIProxy(kubernetesURL, proxyBasePath, clientCA, clientCert, clientK
}
wsp.Director = func(incoming *http.Request, out http.Header) {
out.Set(usernameHeader, incoming.Header.Get(usernameHeader))
if log.GetLevel() >= log.DebugLevel {
if traceRequests {
log.Debugf("Director: adding header %s: %s", usernameHeader, incoming.Header.Get(usernameHeader))
}
out.Set(groupHeader, incoming.Header.Get(groupHeader))
if log.GetLevel() >= log.DebugLevel {
if traceRequests {
log.Debugf("Director: adding header %s: %s", groupHeader, incoming.Header.Get(groupHeader))
}
out.Set("Origin", kubernetesURL)
if log.GetLevel() >= log.DebugLevel {
if traceRequests {
log.Debugf("Director: adding header %s: %s", "Origin", kubernetesURL)
}
}
Expand Down
5 changes: 2 additions & 3 deletions pkg/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import (

"encoding/base64"

log "github.com/sirupsen/logrus"
"github.com/matt-deboer/kapow/pkg/auth"
"github.com/matt-deboer/kapow/pkg/metrics"
"github.com/matt-deboer/kapow/pkg/proxy"
"github.com/matt-deboer/kapow/pkg/templates"
"github.com/matt-deboer/kapow/pkg/version"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli"
"golang.org/x/oauth2"
)
Expand Down Expand Up @@ -79,7 +79,7 @@ func main() {
},
cli.StringFlag{
Name: "public-url",
Usage: "The public-facing URL for this app, used to compose the OIDC redirect_uri",
Usage: "The public-facing URL for this app, used to compose callbacks for IDPs",
EnvVar: envBase + "PUBLIC_URL",
},
cli.StringFlag{
Expand Down Expand Up @@ -290,7 +290,6 @@ func setupAuthenticators(c *cli.Context, authManager *auth.Manager) {
}

oidcHandler, err := auth.NewOIDCHandler(
authManager,
flags["oidc-provider-name"].(string),
flags["public-url"].(string),
provider,
Expand Down
Loading

0 comments on commit 9fbe9f5

Please sign in to comment.