SAML Authentication and IDP Service

Initial SAML based authentication in usermgmt component

Signed-off-by: Akshay Gaikwad <akshay.gaikwad@rafay.co>

Update SAMLAuth middleware

- Get username from request body
- Validate for exiwstng session
- Redirect to IDP authentication when no valid session

Modification in SAML middlewares

- Reduce duplicate code by new function createSAMLMiddleware

Restructure saml package

- Moved SAML Middlewares to middleware.go
- Embed samlsp.Middleware into own struct

Use EntityDAO from common and mock Idp model for testing

Signed-off-by: Akshay Gaikwad <akshay.gaikwad@rafay.co>

Add IDP API definitions

Signed-off-by: Akshay Gaikwad <akshay.gaikwad@rafay.co>

Add interface for IdpService

Add id to UpdateIdp proto message

Add metadata_url to UpdateIdp message and limit to ListIdps rpc

Implement Idp Service methods

Update Idp model

Remove main.go and mocked idp model

Generate ACS URL and SAML SP cert

Change Id type in proto as well as in IDP model

Update IDP model struct tags

Set TimeFormat for IDP service

Update generateSpCert() and generateAcsURL()

Add idpServer which is wrapper around idpService

idpServer is a gRPC controller.

Add back-end validations for idp service
This commit is contained in:
Akshay Gaikwad
2022-01-04 10:27:39 +05:30
parent ccf35cdffd
commit 8464307c1e
11 changed files with 658 additions and 2 deletions

View File

@@ -0,0 +1 @@
APP_HOST_HTTP="http://localhost:8000"

View File

@@ -5,6 +5,7 @@ go 1.17
require (
github.com/RafaySystems/rcloud-base/components/adminsrv v0.0.0-unpublished
github.com/RafaySystems/rcloud-base/components/common v0.0.0-unpublished
github.com/crewjam/saml v0.4.6
github.com/gogo/protobuf v1.3.2
github.com/google/uuid v1.3.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.2
@@ -21,20 +22,27 @@ require (
)
require (
github.com/beevik/etree v1.1.0 // indirect
github.com/crewjam/httperr v0.2.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/golang-jwt/jwt/v4 v4.1.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.6 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/processout/grpc-go-pool v1.2.1 // indirect
github.com/russellhaering/goxmldsig v1.1.1 // indirect
github.com/segmentio/asm v1.1.0 // indirect
github.com/segmentio/encoding v0.3.2 // indirect
github.com/speps/go-hashids v2.0.0+incompatible // indirect

View File

@@ -76,6 +76,8 @@ github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
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/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -122,9 +124,14 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo=
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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
@@ -186,6 +193,8 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
@@ -328,6 +337,7 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
@@ -354,6 +364,9 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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=
@@ -365,6 +378,8 @@ github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
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/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -407,7 +422,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
@@ -435,6 +449,7 @@ github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -474,6 +489,11 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM=
@@ -552,6 +572,7 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
@@ -609,6 +630,7 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -1057,8 +1079,9 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
@@ -1085,6 +1108,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -0,0 +1,34 @@
package models
import (
"time"
"github.com/google/uuid"
"github.com/uptrace/bun"
)
type Idp struct {
bun.BaseModel `bun:"table:authsrv_idp,alias:idp"`
Id uuid.UUID `bun:"id,type:uuid,pk,default:uuid_generate_v4()"`
Name string `bun:"name,notnull,unique"`
Description string `bun:"description"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
ModifiedAt time.Time `bun:"modified_at,notnull,default:current_timestamp"`
IdpName string `bun:"idp_name,notnull"`
Domain string `bun:"domain,notnull,unique"`
AcsURL string `bun:"acs_url,notnull,unique"`
OrganizationId string `bun:"organization_id,type:uuid"`
PartnerId string `bun:"partner_id,type:uuid"`
SsoURL string `bun:"sso_url"`
IdpCert string `bun:"idp_cert"`
SpCert string `bun:"sp_cert"`
SpKey string `bun:"sp_key"`
MetadataURL string `bun:"metadata_url"`
MetadataFilename string `bun:"metadata_filename"`
Metadata []byte `bun:"metadata"`
GroupAttributeName string `bun:"group_attribute_name"`
SaeEnabled bool `bun:"is_sae_enabled"`
Trash bool `bun:"trash,default:false"`
}

View File

@@ -0,0 +1,33 @@
package server
import (
"context"
"github.com/RafaySystems/rcloud-base/components/usermgmt/pkg/service"
rpcv3 "github.com/RafaySystems/rcloud-base/components/usermgmt/proto/rpc/v3"
userv3 "github.com/RafaySystems/rcloud-base/components/usermgmt/proto/types/userpb/v3"
)
type idpServer struct {
service.IdpService
}
func NewIdpServer(is service.IdpService) rpcv3.IdpServer {
return &idpServer{is}
}
func (s *idpServer) CreateIdp(ctx context.Context, idp *userv3.NewIdp) (*userv3.Idp, error) {
return s.IdpService.CreateIdp(ctx, idp)
}
func (s *idpServer) UpdateIdp(ctx context.Context, idp *userv3.UpdateIdp) (*userv3.Idp, error) {
return s.IdpService.UpdateIdp(ctx, idp)
}
func (s *idpServer) GetSpConfigById(ctx context.Context, idpID *userv3.IdpID) (*userv3.SpConfig, error) {
return s.IdpService.GetSpConfigById(ctx, idpID)
}
func (s *idpServer) ListIdps(ctx context.Context, req *userv3.ListIdpsRequest) (*userv3.ListIdpsResponse, error) {
return s.IdpService.ListIdps(ctx, req)
}

View File

@@ -0,0 +1,280 @@
package service
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io/ioutil"
"math/big"
"net/url"
"os"
"time"
"github.com/RafaySystems/rcloud-base/components/common/pkg/persistence/provider/pg"
"github.com/RafaySystems/rcloud-base/components/usermgmt/pkg/internal/models"
userv3 "github.com/RafaySystems/rcloud-base/components/usermgmt/proto/types/userpb/v3"
"github.com/google/uuid"
"github.com/uptrace/bun"
)
const TimeLayout = "2006-01-02T15:04:05.999999Z"
type IdpService interface {
CreateIdp(context.Context, *userv3.NewIdp) (*userv3.Idp, error)
UpdateIdp(context.Context, *userv3.UpdateIdp) (*userv3.Idp, error)
GetSpConfigById(context.Context, *userv3.IdpID) (*userv3.SpConfig, error)
ListIdps(context.Context, *userv3.ListIdpsRequest) (*userv3.ListIdpsResponse, error)
}
type idpService struct {
dao pg.EntityDAO
}
func NewIdpService(db *bun.DB) IdpService {
return &idpService{
dao: pg.NewEntityDAO(db),
}
}
func generateAcsURL(baseURL string) string {
uuid := uuid.New()
acsURL := fmt.Sprintf("%s/%s/", baseURL, uuid.String())
return acsURL
}
// generateSpCert generates self signed certificate. Returns cert and
// private key.
func generateSpCert(host string) (string, string, error) {
// generate private key of type rsa
priv, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return "", "", err
}
privPEM := new(bytes.Buffer)
err = pem.Encode(privPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(priv),
})
if err != nil {
return "", "", err
}
privPEMBytes, err := ioutil.ReadAll(privPEM)
if err != nil {
return "", "", err
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1000),
Subject: pkix.Name{
Organization: []string{"Rafay"},
Country: []string{"US"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(30, 0, 0),
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
DNSNames: []string{host},
}
// generate self sign certificate
cBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
if err != nil {
return "", "", err
}
cPEM := new(bytes.Buffer)
err = pem.Encode(cPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: cBytes,
})
if err != nil {
return "", "", err
}
cPEMBytes, err := ioutil.ReadAll(cPEM)
if err != nil {
return "", "", err
}
return string(cPEMBytes), string(privPEMBytes), nil
}
func (s *idpService) CreateIdp(ctx context.Context, idp *userv3.NewIdp) (*userv3.Idp, error) {
name := idp.GetName()
domain := idp.GetDomain()
e := &models.Idp{}
s.dao.GetByName(ctx, name, e)
if e.Name == name {
return &userv3.Idp{}, fmt.Errorf("DUPLICATE NAME")
}
s.dao.GetX(ctx, "domain", domain, e)
if e.Domain == domain {
return &userv3.Idp{}, fmt.Errorf("DUPLICATE DOMAIN")
}
base, err := url.Parse(os.Getenv("APP_HOST_HTTP"))
if err != nil {
return &userv3.Idp{}, err
}
acsURL := generateAcsURL(base.String())
entity := &models.Idp{
Name: name,
IdpName: idp.GetIdpName(),
Domain: domain,
AcsURL: acsURL,
GroupAttributeName: idp.GetGroupAttributeName(),
SaeEnabled: idp.GetIsSaeEnabled(),
}
if entity.SaeEnabled {
spcert, spkey, err := generateSpCert(base.Host)
if err != nil {
return &userv3.Idp{}, err
}
entity.SpCert = spcert
entity.SpKey = spkey
}
_, err = s.dao.Create(ctx, entity)
if err != nil {
return &userv3.Idp{}, err
}
rv := &userv3.Idp{
Id: entity.Id.String(),
Name: entity.Name,
IdpName: entity.IdpName,
Domain: entity.Domain,
AcsUrl: entity.AcsURL,
SsoUrl: entity.SsoURL,
IdpCert: entity.IdpCert,
SpCert: entity.SpCert,
MetadataUrl: entity.MetadataURL,
MetadataFilename: entity.MetadataFilename,
IsSaeEnabled: entity.SaeEnabled,
GroupAttributeName: entity.GroupAttributeName,
OrganizationId: entity.OrganizationId,
PartnerId: entity.PartnerId,
CreatedAt: entity.CreatedAt.Format(TimeLayout),
ModifiedAt: entity.ModifiedAt.Format(TimeLayout),
}
return rv, nil
}
func (s *idpService) UpdateIdp(ctx context.Context, new *userv3.UpdateIdp) (*userv3.Idp, error) {
id, err := uuid.Parse(new.GetId())
if err != nil {
return &userv3.Idp{}, err
}
entity := &models.Idp{
Id: id,
Name: new.GetName(),
ModifiedAt: time.Now(),
IdpName: new.GetIdpName(),
Domain: new.GetDomain(),
AcsURL: new.GetAcsUrl(),
MetadataURL: new.GetMetadataUrl(),
GroupAttributeName: new.GetGroupAttributeName(),
SaeEnabled: new.GetIsSaeEnabled(),
}
if entity.SaeEnabled {
base, err := url.Parse(os.Getenv("APP_HOST_HTTP"))
if err != nil {
return &userv3.Idp{}, err
}
spcert, spkey, err := generateSpCert(base.Host)
if err != nil {
return &userv3.Idp{}, err
}
entity.SpCert = spcert
entity.SpKey = spkey
}
_, err = s.dao.Update(ctx, id, entity)
if err != nil {
return &userv3.Idp{}, err
}
rv := &userv3.Idp{
Id: entity.Id.String(),
Name: entity.Name,
IdpName: entity.IdpName,
Domain: entity.Domain,
AcsUrl: entity.AcsURL,
SsoUrl: entity.SsoURL,
IdpCert: entity.IdpCert,
SpCert: entity.SpCert,
MetadataUrl: entity.MetadataURL,
MetadataFilename: entity.MetadataFilename,
IsSaeEnabled: entity.SaeEnabled,
GroupAttributeName: entity.GroupAttributeName,
OrganizationId: entity.OrganizationId,
PartnerId: entity.PartnerId,
CreatedAt: entity.CreatedAt.Format(TimeLayout),
ModifiedAt: entity.ModifiedAt.Format(TimeLayout),
}
return rv, nil
}
func (s *idpService) GetSpConfigById(ctx context.Context, idpID *userv3.IdpID) (*userv3.SpConfig, error) {
id, err := uuid.Parse(idpID.GetId())
if err != nil {
return &userv3.SpConfig{}, err
}
entity := &models.Idp{}
_, err = s.dao.GetByID(ctx, id, entity)
if err != nil {
return &userv3.SpConfig{}, err
}
if entity.Id != id {
return &userv3.SpConfig{}, fmt.Errorf("IDP ID DOES NOT EXISTS")
}
rv := &userv3.SpConfig{
NameidFormat: "Email Address",
ConsumerBinding: "HTTP-POST",
AcsUrl: entity.AcsURL,
EntityId: entity.AcsURL,
GroupAttributeName: entity.GroupAttributeName,
SpCert: entity.SpCert,
}
return rv, nil
}
func (s *idpService) ListIdps(ctx context.Context, req *userv3.ListIdpsRequest) (*userv3.ListIdpsResponse, error) {
entities := []*models.Idp{}
var orgID uuid.NullUUID
var parID uuid.NullUUID
s.dao.List(ctx, parID, orgID, entities)
// Get idps only till limit
var result []*userv3.Idp
for _, entity := range entities {
e := &userv3.Idp{
Id: entity.Id.String(),
Name: entity.Name,
IdpName: entity.IdpName,
Domain: entity.Domain,
AcsUrl: entity.AcsURL,
SsoUrl: entity.SsoURL,
IdpCert: entity.IdpCert,
SpCert: entity.SpCert,
MetadataUrl: entity.MetadataURL,
MetadataFilename: entity.MetadataFilename,
IsSaeEnabled: entity.SaeEnabled,
GroupAttributeName: entity.GroupAttributeName,
OrganizationId: entity.OrganizationId,
PartnerId: entity.PartnerId,
CreatedAt: entity.CreatedAt.Format(TimeLayout),
ModifiedAt: entity.ModifiedAt.Format(TimeLayout),
}
result = append(result, e)
}
rv := &userv3.ListIdpsResponse{
Count: int32(len(entities)),
Next: 0,
Previous: 0,
Result: result,
}
return rv, nil
}

View File

@@ -0,0 +1,167 @@
package saml
import (
"context"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"net/http"
"net/url"
"os"
"strings"
"github.com/RafaySystems/rcloud-base/components/usermgmt/pkg/internal/models"
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
)
func newSAMLMiddlewareFromIDP(idp models.Idp) (*SAMLMiddleware, error) {
rootURL, err := url.Parse(os.Getenv("APP_HOST_HTTP"))
if err != nil {
return nil, err
}
var idpMetadata *saml.EntityDescriptor
if len(idp.Metadata) == 0 {
idpMetadataURL, err := url.Parse(idp.MetadataURL)
if err != nil {
return nil, err
}
idpMetadata, err = samlsp.FetchMetadata(context.Background(), http.DefaultClient,
*idpMetadataURL)
} else {
idpMetadata, err = samlsp.ParseMetadata(idp.Metadata)
if err != nil {
return nil, err
}
}
acsURL, err := url.Parse(idp.AcsURL)
if err != nil {
return nil, err
}
keyPair, err := tls.X509KeyPair([]byte(idp.SpCert), []byte(idp.SpKey))
if err != nil {
return nil, err
}
keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
if err != nil {
return nil, err
}
opts := samlsp.Options{
EntityID: "",
URL: *rootURL,
Key: keyPair.PrivateKey.(*rsa.PrivateKey),
Certificate: keyPair.Leaf,
AllowIDPInitiated: false,
DefaultRedirectURI: "/prelogin",
IDPMetadata: idpMetadata,
SignRequest: false,
}
sp := samlsp.DefaultServiceProvider(opts)
sp.AcsURL = *acsURL
m := &samlsp.Middleware{
ServiceProvider: sp,
Binding: "",
ResponseBinding: saml.HTTPPostBinding,
OnError: samlsp.DefaultOnError,
Session: samlsp.DefaultSessionProvider(opts),
}
m.RequestTracker = samlsp.DefaultRequestTracker(opts, &m.ServiceProvider)
if opts.UseArtifactResponse {
m.ResponseBinding = saml.HTTPArtifactBinding
}
return &SAMLMiddleware{m}, nil
}
// SAMLAuth is an authentication middleware.
func (s *SAMLService) SAMLAuth(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "failed to parse form data", http.StatusBadRequest)
return
}
username := r.PostForm.Get("username")
if !strings.Contains(username, "@") {
http.Error(w, "Invalid email address", http.StatusBadRequest)
return
}
domain := strings.SplitN(username, "@", 2)[1]
entity, err := s.EntityDAO.GetX(context.Background(), "domain", domain, &models.Idp{})
if err != nil {
http.Error(w, "No idp found for domain", http.StatusInternalServerError)
return
}
idp, ok := entity.(models.Idp)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
m, err := newSAMLMiddlewareFromIDP(idp)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
session, err := m.Session.GetSession(r)
if session != nil {
r = r.WithContext(samlsp.ContextWithSession(r.Context(), session))
w.Write([]byte("authentiated successfully"))
return
}
if err == samlsp.ErrNoSession {
m.HandleStartAuthFlow(w, r)
return
} else {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
})
}
// ServeACS performs SAML Response assertions.
func (s *SAMLService) ServeACS(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
base, _ := url.Parse(os.Getenv("APP_HOST_HTTP"))
acsURL := base.ResolveReference(r.URL)
entity, err := s.EntityDAO.GetX(context.Background(), "acs_url", acsURL.String(), &models.Idp{})
if err != nil {
http.Error(w, "No Idp for ACS URL", http.StatusInternalServerError)
return
}
idp, ok := entity.(models.Idp)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
m, err := newSAMLMiddlewareFromIDP(idp)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
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)
return
}

View File

@@ -0,0 +1,21 @@
package saml
import (
pg "github.com/RafaySystems/rcloud-base/components/common/pkg/persistence/provider/pg"
"github.com/crewjam/saml/samlsp"
"github.com/uptrace/bun"
)
type SAMLMiddleware struct {
*samlsp.Middleware
}
type SAMLService struct {
EntityDAO pg.EntityDAO
}
func NewSAMLService(db *bun.DB) *SAMLService {
return &SAMLService{
EntityDAO: pg.NewEntityDAO(db),
}
}

View File

@@ -0,0 +1 @@
package saml

View File

@@ -0,0 +1,22 @@
syntax = "proto3";
package rafay.dev.rpc.v3;
import "proto/types/userpb/v3/idp.proto";
service Idp {
// endpoint POST /auth/v1/sso/idp
rpc CreateIdp(rafay.dev.types.user.v3.NewIdp) returns (rafay.dev.types.user.v3.Idp) {};
// endpoint PUT /auth/v1/sso/idp
rpc UpdateIdp(rafay.dev.types.user.v3.UpdateIdp) returns (rafay.dev.types.user.v3.Idp) {};
// endpoint /auth/v1/sso/idp/dk351mn/spconfig/
rpc GetSpConfigById(rafay.dev.types.user.v3.IdpID) returns (rafay.dev.types.user.v3.SpConfig) {};
// endpoint /auth/v1/sso/idp/?limit=1000
rpc ListIdps(rafay.dev.types.user.v3.ListIdpsRequest) returns (rafay.dev.types.user.v3.ListIdpsResponse) {};
// endpooint /auth/v1/sso/idp/5m16w2y/upload_metadata/
// file content as request payload and response is Idp
}

View File

@@ -0,0 +1,64 @@
syntax = "proto3";
package rafay.dev.types.user.v3;
message Idp {
string id = 1;
string name = 2;
string idp_name = 3;
string domain = 4;
string acs_url = 5;
string sso_url = 6;
string idp_cert = 7;
string sp_cert = 8;
string metadata_url = 9;
string metadata_filename = 10;
bool is_sae_enabled = 11;
string group_attribute_name = 12;
string organization_id = 13;
string partner_id = 14;
string created_at = 15;
string modified_at = 16;
}
message NewIdp {
string domain = 1;
string group_attribute_name = 2;
string idp_name = 3;
bool is_sae_enabled = 4;
string name = 5;
}
message UpdateIdp {
string id = 1;
string acs_url = 2;
string domain = 3;
string group_attribute_name = 4;
string idp_name = 5;
bool is_sae_enabled = 6;
string metadata_url = 7;
string name = 8;
}
message IdpID {
string id = 1;
}
message SpConfig {
string nameid_format = 1;
string consumer_binding = 2;
string acs_url = 3;
string entity_id = 4;
string group_attribute_name = 5;
string sp_cert = 6;
}
message ListIdpsResponse {
int32 count = 1;
int32 next = 2;
int32 previous = 3;
repeated Idp result = 4;
}
message ListIdpsRequest {
int32 limit = 1;
}