diff --git a/authz/authz.go b/authz/authz.go index e5d45030..52321ad7 100644 --- a/authz/authz.go +++ b/authz/authz.go @@ -95,6 +95,8 @@ p, *, *, POST, /api/reset-email-or-phone, *, * p, *, *, POST, /api/upload-resource, *, * p, *, *, GET, /.well-known/openid-configuration, *, * p, *, *, *, /api/certs, *, * +p, *, *, GET, /api/get-saml-login, *, * +p, *, *, POST, /api/acs, *, * ` sa := stringadapter.NewAdapter(ruleText) diff --git a/conf/app.conf b/conf/app.conf index 330c08b2..ede68384 100644 --- a/conf/app.conf +++ b/conf/app.conf @@ -14,4 +14,6 @@ httpProxy = "127.0.0.1:10808" verificationCodeTimeout = 10 initScore = 2000 logPostOnly = true -oidcOrigin = "https://door.casbin.com" \ No newline at end of file +oidcOrigin = "https://door.casbin.com" +samlOrigin = "http://localhost:8000" +samlRequestOrigin = "http://localhost:7001" \ No newline at end of file diff --git a/controllers/account.go b/controllers/account.go index b4c66e28..b9260252 100644 --- a/controllers/account.go +++ b/controllers/account.go @@ -53,6 +53,9 @@ type RequestForm struct { PhonePrefix string `json:"phonePrefix"` AutoSignin bool `json:"autoSignin"` + + RelayState string `json:"relayState"` + SamlResponse string `json:"samlResponse"` } type Response struct { diff --git a/controllers/auth.go b/controllers/auth.go index f65beab0..c1558487 100644 --- a/controllers/auth.go +++ b/controllers/auth.go @@ -17,6 +17,7 @@ package controllers import ( "encoding/json" "fmt" + "net/url" "strconv" "strings" "time" @@ -212,44 +213,60 @@ func (c *ApiController) Login() { return } - idProvider := idp.GetIdProvider(provider.Type, provider.ClientId, provider.ClientSecret, form.RedirectUri) - if idProvider == nil { - c.ResponseError(fmt.Sprintf("The provider type: %s is not supported", provider.Type)) - return - } + userInfo := &idp.UserInfo{} + if provider.Category == "SAML" { + // SAML + userInfo.Id, err = object.ParseSamlResponse(form.SamlResponse) + if err != nil { + c.ResponseError(err.Error()) + return + } + } else if provider.Category == "OAuth" { + // OAuth + idProvider := idp.GetIdProvider(provider.Type, provider.ClientId, provider.ClientSecret, form.RedirectUri) + if idProvider == nil { + c.ResponseError(fmt.Sprintf("The provider type: %s is not supported", provider.Type)) + return + } - setHttpClient(idProvider, provider.Type) + setHttpClient(idProvider, provider.Type) - if form.State != beego.AppConfig.String("authState") && form.State != application.Name { - c.ResponseError(fmt.Sprintf("state expected: \"%s\", but got: \"%s\"", beego.AppConfig.String("authState"), form.State)) - return - } + if form.State != beego.AppConfig.String("authState") && form.State != application.Name { + c.ResponseError(fmt.Sprintf("state expected: \"%s\", but got: \"%s\"", beego.AppConfig.String("authState"), form.State)) + return + } - // https://github.com/golang/oauth2/issues/123#issuecomment-103715338 - token, err := idProvider.GetToken(form.Code) - if err != nil { - c.ResponseError(err.Error()) - return - } + // https://github.com/golang/oauth2/issues/123#issuecomment-103715338 + token, err := idProvider.GetToken(form.Code) + if err != nil { + c.ResponseError(err.Error()) + return + } - if !token.Valid() { - c.ResponseError("Invalid token") - return - } + if !token.Valid() { + c.ResponseError("Invalid token") + return + } - userInfo, err := idProvider.GetUserInfo(token) - if err != nil { - c.ResponseError(fmt.Sprintf("Failed to login in: %s", err.Error())) - return + userInfo, err = idProvider.GetUserInfo(token) + if err != nil { + c.ResponseError(fmt.Sprintf("Failed to login in: %s", err.Error())) + return + } } if form.Method == "signup" { - user := object.GetUserByField(application.Organization, provider.Type, userInfo.Id) - if user == nil { - user = object.GetUserByField(application.Organization, provider.Type, userInfo.Username) - } - if user == nil { - user = object.GetUserByField(application.Organization, "name", userInfo.Username) + user := &object.User{} + if provider.Category == "SAML" { + user = object.GetUserByField(application.Organization, "id", userInfo.Id) + } else if provider.Category == "OAuth" { + user := object.GetUserByField(application.Organization, provider.Type, userInfo.Id) + if user == nil { + user = object.GetUserByField(application.Organization, provider.Type, userInfo.Username) + } + if user == nil { + user = object.GetUserByField(application.Organization, "name", userInfo.Username) + } } if user != nil && user.IsDeleted == false { @@ -265,7 +282,7 @@ func (c *ApiController) Login() { record.Organization = application.Organization record.User = user.Name go object.AddRecord(record) - } else { + } else if provider.Category == "OAuth" { // Sign up via OAuth if !application.EnableSignUp { c.ResponseError(fmt.Sprintf("The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support", provider.Type, userInfo.Username, userInfo.DisplayName)) @@ -279,7 +296,7 @@ func (c *ApiController) Login() { properties := map[string]string{} properties["no"] = strconv.Itoa(len(object.GetUsers(application.Organization)) + 2) - user := &object.User{ + user = &object.User{ Owner: application.Organization, Name: userInfo.Username, CreatedTime: util.GetCurrentTime(), @@ -297,11 +314,11 @@ func (c *ApiController) Login() { SignupApplication: application.Name, Properties: properties, } - object.AddUser(user) - // sync info from 3rd-party if possible object.SetUserOAuthProperties(organization, user, provider.Type, userInfo) + object.AddUser(user) + object.LinkUserAccount(user, provider.Type, userInfo.Id) resp = c.HandleLoggedIn(application, user, &form) @@ -310,6 +327,8 @@ func (c *ApiController) Login() { record.Organization = application.Organization record.User = user.Name go object.AddRecord(record) + } else if provider.Category == "SAML" { + resp = &Response{Status: "error", Msg: "The account does not exist"} } //resp = &Response{Status: "ok", Msg: "", Data: res} } else { // form.Method != "signup" @@ -355,3 +374,21 @@ func (c *ApiController) Login() { c.Data["json"] = resp c.ServeJSON() } + +func (c *ApiController) GetSamlLogin() { + providerId := c.Input().Get("id") + authURL, err := object.GenerateSamlLoginUrl(providerId) + if err != nil { + c.ResponseError(err.Error()) + } + c.ResponseOk(authURL) +} + +func (c *ApiController) HandleSamlLogin() { + relayState := c.Input().Get("RelayState") + samlResponse := c.Input().Get("SAMLResponse") + samlResponse = url.QueryEscape(samlResponse) + targetUrl := fmt.Sprintf("%s/callback/saml?replayState=%s&samlResponse=%s", + beego.AppConfig.String("samlRequestOrigin"), relayState, samlResponse) + c.Redirect(targetUrl, 303) +} diff --git a/go.mod b/go.mod index 62ec40bd..6c6a8a0f 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,8 @@ require ( github.com/mileusna/crontab v1.0.1 github.com/qiangmzsx/string-adapter/v2 v2.1.0 github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76 + github.com/russellhaering/gosaml2 v0.6.0 + github.com/russellhaering/goxmldsig v1.1.1 github.com/satori/go.uuid v1.2.0 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect github.com/thanhpk/randstr v1.0.4 diff --git a/go.sum b/go.sum index 02c5e475..97a81649 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7 github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ= github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU= +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/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -89,6 +91,7 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U= github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c= github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY= 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= @@ -203,6 +206,9 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= @@ -217,14 +223,20 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv 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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 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/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattermost/xml-roundtrip-validator v0.0.0-20201208211235-fe770d50d911 h1:erppMjjp69Rertg1zlgRbLJH1u+eCmRPxKjMZ5I8/Ro= +github.com/mattermost/xml-roundtrip-validator v0.0.0-20201208211235-fe770d50d911/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= @@ -240,7 +252,6 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -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/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -252,6 +263,7 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -278,6 +290,14 @@ github.com/qiangmzsx/string-adapter/v2 v2.1.0/go.mod h1:PElPB7b7HnGKTsuADAffFpOQ github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76 h1:J2Xj92efYLxPl3BiibgEDEUiMsCBzwTurE/8JjD8CG4= github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76/go.mod h1:JhtPzUhP5KGtCB2yksmxuYAD4hEWw4qGQJpucjsm3U0= 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/gosaml2 v0.6.0 h1:OED8FLgczXxXAPlKhnJHQfmEig52tDX2qeXdPtZRIKc= +github.com/russellhaering/gosaml2 v0.6.0/go.mod h1:CtzxpPr4+bevsATaqR0rw3aqrNlX274b+3C6vFTLCk8= +github.com/russellhaering/goxmldsig v1.1.0/go.mod h1:QK8GhXPB3+AfuCrfo0oRISa9NfzeCpWmxeGnqEpDF9o= +github.com/russellhaering/goxmldsig v1.1.1 h1:vI0r2osGF1A9PLvsGdPUAGwEIrKa4Pj5sesSBsebIxM= +github.com/russellhaering/goxmldsig v1.1.1/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo= @@ -586,8 +606,9 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod 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 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= @@ -607,8 +628,9 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 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= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/object/provider.go b/object/provider.go index b060815c..edf949af 100644 --- a/object/provider.go +++ b/object/provider.go @@ -43,11 +43,15 @@ type Provider struct { TemplateCode string `xorm:"varchar(100)" json:"templateCode"` AppId string `xorm:"varchar(100)" json:"appId"` - Endpoint string `xorm:"varchar(100)" json:"endpoint"` + Endpoint string `xorm:"varchar(1000)" json:"endpoint"` IntranetEndpoint string `xorm:"varchar(100)" json:"intranetEndpoint"` Domain string `xorm:"varchar(100)" json:"domain"` Bucket string `xorm:"varchar(100)" json:"bucket"` + Metadata string `xorm:"mediumtext" json:"metadata"` + IdP string `xorm:"mediumtext" json:"idP"` + IssuerUrl string `xorm:"varchar(100)" json:"issuerUrl"` + ProviderUrl string `xorm:"varchar(200)" json:"providerUrl"` } diff --git a/object/provider_item.go b/object/provider_item.go index 1e68495a..af63839c 100644 --- a/object/provider_item.go +++ b/object/provider_item.go @@ -34,7 +34,7 @@ func (application *Application) GetProviderItem(providerName string) *ProviderIt } func (pi *ProviderItem) IsProviderVisible() bool { - return pi.Provider.Category == "OAuth" + return pi.Provider.Category == "OAuth" || pi.Provider.Category == "SAML" } func (pi *ProviderItem) isProviderPrompted() bool { diff --git a/object/saml.go b/object/saml.go new file mode 100644 index 00000000..69071bd0 --- /dev/null +++ b/object/saml.go @@ -0,0 +1,99 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "crypto/x509" + "encoding/base64" + "fmt" + "net/url" + "regexp" + "strings" + + "github.com/astaxie/beego" + saml2 "github.com/russellhaering/gosaml2" + dsig "github.com/russellhaering/goxmldsig" +) + +func ParseSamlResponse(samlResponse string) (string, error) { + samlResponse, _ = url.QueryUnescape(samlResponse) + sp, err := buildSp(nil, samlResponse) + if err != nil { + return "", err + } + assertionInfo, err := sp.RetrieveAssertionInfo(samlResponse) + if err != nil { + panic(err) + } + return assertionInfo.NameID, nil +} + +func GenerateSamlLoginUrl(id string) (string, error) { + provider := GetProvider(id) + if provider.Category != "SAML" { + return "", fmt.Errorf("Provider %s's category is not SAML", provider.Name) + } + sp, err := buildSp(provider, "") + if err != nil { + return "", err + } + authURL, err := sp.BuildAuthURL("") + if err != nil { + return "", err + } + return authURL, nil +} + +func buildSp(provider *Provider, samlResponse string) (*saml2.SAMLServiceProvider, error) { + certStore := dsig.MemoryX509CertificateStore{ + Roots: []*x509.Certificate{}, + } + samlOrigin := beego.AppConfig.String("samlOrigin") + certEncodedData := "" + if samlResponse != "" { + de, err := base64.StdEncoding.DecodeString(samlResponse) + if err != nil { + panic(err) + } + deStr := strings.Replace(string(de), "\n", "", -1) + res := regexp.MustCompile(`(.*?)`).FindAllStringSubmatch(deStr, -1) + str := res[0][0] + certEncodedData = str[20 : len(str)-21] + } else if provider != nil { + certEncodedData = provider.IdP + } + certData, err := base64.StdEncoding.DecodeString(certEncodedData) + if err != nil { + return nil, err + } + idpCert, err := x509.ParseCertificate(certData) + if err != nil { + return nil, err + } + certStore.Roots = append(certStore.Roots, idpCert) + sp := &saml2.SAMLServiceProvider{ + ServiceProviderIssuer: fmt.Sprintf("%s/api/acs", samlOrigin), + AssertionConsumerServiceURL: fmt.Sprintf("%s/api/acs", samlOrigin), + IDPCertificateStore: &certStore, + } + if provider != nil { + randomKeyStore := dsig.RandomKeyStoreForTest() + sp.IdentityProviderSSOURL = provider.Endpoint + sp.IdentityProviderIssuer = provider.IssuerUrl + sp.SignAuthnRequests = false + sp.SPKeyStore = randomKeyStore + } + return sp, nil +} diff --git a/routers/router.go b/routers/router.go index e1ccf30f..729f47bc 100644 --- a/routers/router.go +++ b/routers/router.go @@ -51,6 +51,8 @@ func initAPI() { beego.Router("/api/logout", &controllers.ApiController{}, "POST:Logout") beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount") beego.Router("/api/unlink", &controllers.ApiController{}, "POST:Unlink") + beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin") + beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin") beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations") beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization") diff --git a/web/src/App.js b/web/src/App.js index 6493486e..1648639e 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -53,6 +53,7 @@ import SelectLanguageBox from './SelectLanguageBox'; import i18next from 'i18next'; import PromptPage from "./auth/PromptPage"; import OdicDiscoveryPage from "./auth/OidcDiscoveryPage"; +import SamlCallback from './auth/SamlCallback'; const { Header, Footer } = Layout; @@ -547,6 +548,7 @@ class App extends Component { {this.onUpdateAccount(account)}} />}/> {this.onUpdateAccount(account)}} />}/> + this.renderHomeIfLoggedIn()}/> this.renderHomeIfLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> diff --git a/web/src/ProviderEditPage.js b/web/src/ProviderEditPage.js index 57579aa4..bee94683 100644 --- a/web/src/ProviderEditPage.js +++ b/web/src/ProviderEditPage.js @@ -18,6 +18,8 @@ import {LinkOutlined} from "@ant-design/icons"; import * as ProviderBackend from "./backend/ProviderBackend"; import * as Setting from "./Setting"; import i18next from "i18next"; +import { authConfig } from "./auth/Auth"; +import copy from 'copy-to-clipboard'; const { Option } = Select; const { TextArea } = Input; @@ -103,6 +105,10 @@ class ProviderEditPage extends React.Component { {id: 'Tencent Cloud COS', name: 'Tencent Cloud COS'}, ] ); + } else if (provider.category === "SAML") { + return ([ + {id: 'Aliyun IDaaS', name: 'Aliyun IDaaS'}, + ]); } else { return []; } @@ -156,6 +162,17 @@ class ProviderEditPage extends React.Component { ; } + loadSamlConfiguration() { + var parser = new DOMParser(); + var xmlDoc = parser.parseFromString(this.state.provider.metadata, "text/xml"); + var cert = xmlDoc.getElementsByTagName("ds:X509Certificate")[0].childNodes[0].nodeValue; + var endpoint = xmlDoc.getElementsByTagName("md:SingleSignOnService")[0].getAttribute("Location"); + var issuerUrl = xmlDoc.getElementsByTagName("md:EntityDescriptor")[0].getAttribute("entityID"); + this.updateProviderField("idP", cert); + this.updateProviderField("endpoint", endpoint); + this.updateProviderField("issuerUrl", issuerUrl); + } + renderProvider() { return ( { @@ -210,6 +229,7 @@ class ProviderEditPage extends React.Component { {id: 'Email', name: 'Email'}, {id: 'SMS', name: 'SMS'}, {id: 'Storage', name: 'Storage'}, + {id: 'SAML', name: 'SAML'}, ].map((providerCategory, index) => ) } @@ -391,6 +411,96 @@ class ProviderEditPage extends React.Component { + ) : this.state.provider.category === "SAML" ? ( + + + + {Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} : + + +