diff --git a/authz/authz.go b/authz/authz.go index acf0e0631e62..91f28aa6103b 100644 --- a/authz/authz.go +++ b/authz/authz.go @@ -101,6 +101,7 @@ p, *, *, GET, /.well-known/openid-configuration, *, * p, *, *, *, /.well-known/jwks, *, * p, *, *, GET, /api/get-saml-login, *, * p, *, *, POST, /api/acs, *, * +p, *, *, GET, /api/saml/metadata, *, * p, *, *, *, /cas, *, * ` diff --git a/controllers/account.go b/controllers/account.go index ae4a834e9bdc..62d7ce225b2f 100644 --- a/controllers/account.go +++ b/controllers/account.go @@ -29,6 +29,7 @@ const ( ResponseTypeCode = "code" ResponseTypeToken = "token" ResponseTypeIdToken = "id_token" + ResponseTypeSaml = "saml" ResponseTypeCas = "cas" ) @@ -61,6 +62,7 @@ type RequestForm struct { AutoSignin bool `json:"autoSignin"` RelayState string `json:"relayState"` + SamlRequest string `json:"samlRequest"` SamlResponse string `json:"samlResponse"` } diff --git a/controllers/auth.go b/controllers/auth.go index 8348e85a5b9b..6b70397a856d 100644 --- a/controllers/auth.go +++ b/controllers/auth.go @@ -83,6 +83,13 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob resp = tokenToResponse(token) } + } else if form.Type == ResponseTypeSaml { // saml flow + res, redirectUrl, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host) + if err != nil { + c.ResponseError(err.Error(), nil) + return + } + resp = &Response{Status: "ok", Msg: "", Data: res, Data2: redirectUrl} } else if form.Type == ResponseTypeCas { //not oauth but CAS SSO protocol service := c.Input().Get("service") diff --git a/controllers/saml.go b/controllers/saml.go new file mode 100644 index 000000000000..414d9bceb796 --- /dev/null +++ b/controllers/saml.go @@ -0,0 +1,33 @@ +// Copyright 2022 The Casdoor 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 controllers + +import ( + "fmt" + + "github.com/casdoor/casdoor/object" +) + +func (c *ApiController) GetSamlMeta() { + host := c.Ctx.Request.Host + paramApp := c.Input().Get("application") + application := object.GetApplication(paramApp) + if application == nil { + c.ResponseError(fmt.Sprintf("err: application %s not found", paramApp)) + } + metadata, _ := object.GetSamlMeta(application, host) + c.Data["xml"] = metadata + c.ServeXML() +} diff --git a/go.mod b/go.mod index 4c80ae5014b4..7a7000ec0e7f 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,12 @@ module github.com/casdoor/casdoor go 1.16 require ( + github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b github.com/aliyun/aliyun-oss-go-sdk v2.1.6+incompatible // indirect github.com/astaxie/beego v1.12.3 github.com/aws/aws-sdk-go v1.37.30 github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect + github.com/beevik/etree v1.1.0 github.com/casbin/casbin/v2 v2.30.1 github.com/casbin/xorm-adapter/v2 v2.5.1 github.com/casdoor/go-sms-sender v0.2.0 @@ -19,12 +21,15 @@ require ( github.com/golang-jwt/jwt/v4 v4.2.0 github.com/google/uuid v1.2.0 github.com/jinzhu/configor v1.2.1 // indirect + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/lestrrat-go/jwx v0.9.0 + github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/qiangmzsx/string-adapter/v2 v2.1.0 github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76 github.com/robfig/cron/v3 v3.0.1 github.com/russellhaering/gosaml2 v0.6.0 github.com/russellhaering/goxmldsig v1.1.1 + github.com/satori/go.uuid v1.2.0 github.com/smartystreets/goconvey v1.6.4 // indirect github.com/stretchr/testify v1.7.0 github.com/tealeg/xlsx v1.0.5 diff --git a/go.sum b/go.sum index 69dfd3af5bcc..69525e7fcfa5 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,8 @@ github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8L github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b h1:EgJ6N2S0h1WfFIjU5/VVHWbMSVYXAluop97Qxpr/lfQ= +github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b/go.mod h1:3SAoF0F5EbcOuBD5WT9nYkbIJieBS84cUQXADbXeBsU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -235,6 +237,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -254,6 +258,8 @@ 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/ma314smith/signedxml v0.0.0-20210628192057-abc5b481ae1c h1:UPJygtyk491bJJ/DnRJFuzcq9Dl9NSeFrJ7VdiRzMxc= +github.com/ma314smith/signedxml v0.0.0-20210628192057-abc5b481ae1c/go.mod h1:KEgVcb43+f5KFUH/x6Vd3NROG0AIL2CuKMrIqYsmx6E= github.com/markbates/going v1.0.0 h1:DQw0ZP7NbNlFGcKbcE/IVSOAFzScxRtLpd0rLMzLhq0= github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA= github.com/mattermost/xml-roundtrip-validator v0.0.0-20201208211235-fe770d50d911 h1:erppMjjp69Rertg1zlgRbLJH1u+eCmRPxKjMZ5I8/Ro= @@ -274,6 +280,8 @@ github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c h1:3wkDRdxK92dF+c1ke github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= 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/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= +github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= 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= github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= diff --git a/object/application.go b/object/application.go index 37bfd7644a1f..485d0b638511 100644 --- a/object/application.go +++ b/object/application.go @@ -300,7 +300,6 @@ func (application *Application) GetId() string { func CheckRedirectUriValid(application *Application, redirectUri string) bool { var validUri = false for _, tmpUri := range application.RedirectUris { - fmt.Println(tmpUri, redirectUri) if strings.Contains(redirectUri, tmpUri) { validUri = true break diff --git a/object/saml_idp.go b/object/saml_idp.go new file mode 100644 index 000000000000..01b4182ef43e --- /dev/null +++ b/object/saml_idp.go @@ -0,0 +1,274 @@ +// Copyright 2022 The Casdoor 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 ( + "bytes" + "compress/flate" + "crypto" + "crypto/rsa" + "encoding/base64" + "encoding/pem" + "encoding/xml" + "fmt" + "io" + "time" + + "github.com/RobotsAndPencils/go-saml" + "github.com/astaxie/beego" + "github.com/beevik/etree" + "github.com/golang-jwt/jwt/v4" + dsig "github.com/russellhaering/goxmldsig" + uuid "github.com/satori/go.uuid" +) + +func NewSamlResponse(user *User, host string, publicKey string, destination string, iss string, redirectUri []string) (*etree.Element, error) { + samlResponse := &etree.Element{ + Space: "samlp", + Tag: "Response", + } + now := time.Now().UTC().Format(time.RFC3339) + expireTime := time.Now().UTC().Add(time.Hour * 24).Format(time.RFC3339) + samlResponse.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol") + samlResponse.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion") + arId := uuid.NewV4() + + samlResponse.CreateAttr("ID", fmt.Sprintf("_%s", arId)) + samlResponse.CreateAttr("Version", "2.0") + samlResponse.CreateAttr("IssueInstant", now) + samlResponse.CreateAttr("Destination", destination) + samlResponse.CreateAttr("InResponseTo", fmt.Sprintf("Casdoor_%s", arId)) + samlResponse.CreateElement("saml:Issuer").SetText(host) + + samlResponse.CreateElement("samlp:Status").CreateElement("samlp:StatusCode").CreateAttr("Value", "urn:oasis:names:tc:SAML:2.0:status:Success") + + assertion := samlResponse.CreateElement("saml:Assertion") + assertion.CreateAttr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + assertion.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema") + assertion.CreateAttr("ID", fmt.Sprintf("_%s", uuid.NewV4())) + assertion.CreateAttr("Version", "2.0") + assertion.CreateAttr("IssueInstant", now) + assertion.CreateElement("saml:Issuer").SetText(host) + subject := assertion.CreateElement("saml:Subject") + subject.CreateElement("saml:NameID").SetText(user.Email) + subjectConfirmation := subject.CreateElement("saml:SubjectConfirmation") + subjectConfirmation.CreateAttr("Method", "urn:oasis:names:tc:SAML:2.0:cm:bearer") + subjectConfirmationData := subjectConfirmation.CreateElement("saml:SubjectConfirmationData") + subjectConfirmationData.CreateAttr("InResponseTo", fmt.Sprintf("_%s", arId)) + subjectConfirmationData.CreateAttr("Recipient", destination) + subjectConfirmationData.CreateAttr("NotOnOrAfter", expireTime) + condition := assertion.CreateElement("saml:Conditions") + condition.CreateAttr("NotBefore", now) + condition.CreateAttr("NotOnOrAfter", expireTime) + audience := condition.CreateElement("saml:AudienceRestriction") + audience.CreateElement("saml:Audience").SetText(iss) + for _, value := range redirectUri { + audience.CreateElement("saml:Audience").SetText(value) + } + authnStatement := assertion.CreateElement("saml:AuthnStatement") + authnStatement.CreateAttr("AuthnInstant", now) + authnStatement.CreateAttr("SessionIndex", fmt.Sprintf("_%s", uuid.NewV4())) + authnStatement.CreateAttr("SessionNotOnOrAfter", expireTime) + authnStatement.CreateElement("saml:AuthnContext").CreateElement("saml:AuthnContextClassRef").SetText("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport") + + attributes := assertion.CreateElement("saml:AttributeStatement") + email := attributes.CreateElement("saml:Attribute") + email.CreateAttr("Name", "Email") + email.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic") + email.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.Email) + name := attributes.CreateElement("saml:Attribute") + name.CreateAttr("Name", "Name") + name.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic") + name.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.Name) + displayName := attributes.CreateElement("saml:Attribute") + displayName.CreateAttr("Name", "DisplayName") + displayName.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic") + displayName.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.DisplayName) + + return samlResponse, nil + +} + +type X509Key struct { + X509Certificate string + PrivateKey string +} + +func (x X509Key) GetKeyPair() (privateKey *rsa.PrivateKey, cert []byte, err error) { + cert, _ = base64.StdEncoding.DecodeString(x.X509Certificate) + privateKey, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(x.PrivateKey)) + return privateKey, cert, err +} + +//SAML METADATA +type IdpEntityDescriptor struct { + XMLName xml.Name `xml:"EntityDescriptor"` + DS string `xml:"xmlns:ds,attr"` + XMLNS string `xml:"xmlns,attr"` + MD string `xml:"xmlns:md,attr"` + EntityId string `xml:"entityID,attr"` + + IdpSSODescriptor IdpSSODescriptor `xml:"IDPSSODescriptor"` +} + +type KeyInfo struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# KeyInfo"` + X509Data X509Data `xml:",innerxml"` +} + +type X509Data struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# X509Data"` + X509Certificate X509Certificate `xml:",innerxml"` +} + +type X509Certificate struct { + XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# X509Certificate"` + Cert string `xml:",innerxml"` +} + +type KeyDescriptor struct { + XMLName xml.Name `xml:"KeyDescriptor"` + Use string `xml:"use,attr"` + KeyInfo KeyInfo `xml:"KeyInfo"` +} + +type IdpSSODescriptor struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"` + ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` + SigningKeyDescriptor KeyDescriptor + NameIDFormats []NameIDFormat `xml:"NameIDFormat"` + SingleSignOnService SingleSignOnService `xml:"SingleSignOnService"` + Attribute []Attribute `xml:"Attribute"` +} + +type NameIDFormat struct { + XMLName xml.Name + Value string `xml:",innerxml"` +} + +type SingleSignOnService struct { + XMLName xml.Name + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` +} + +type Attribute struct { + XMLName xml.Name + Name string `xml:"Name,attr"` + NameFormat string `xml:"NameFormat,attr"` + FriendlyName string `xml:"FriendlyName,attr"` + Xmlns string `xml:"xmlns,attr"` +} + +func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) { + //_, originBackend := getOriginFromHost(host) + cert := getCertByApplication(application) + block, _ := pem.Decode([]byte(cert.PublicKey)) + publicKey := base64.StdEncoding.EncodeToString(block.Bytes) + + origin := beego.AppConfig.String("origin") + originFrontend, originBackend := getOriginFromHost(host) + if origin != "" { + originBackend = origin + } + d := IdpEntityDescriptor{ + XMLName: xml.Name{ + Local: "md:EntityDescriptor", + }, + DS: "http://www.w3.org/2000/09/xmldsig#", + XMLNS: "urn:oasis:names:tc:SAML:2.0:metadata", + MD: "urn:oasis:names:tc:SAML:2.0:metadata", + EntityId: originBackend, + IdpSSODescriptor: IdpSSODescriptor{ + SigningKeyDescriptor: KeyDescriptor{ + Use: "signing", + KeyInfo: KeyInfo{ + X509Data: X509Data{ + X509Certificate: X509Certificate{ + Cert: publicKey, + }, + }, + }, + }, + NameIDFormats: []NameIDFormat{ + {Value: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"}, + {Value: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"}, + {Value: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"}, + }, + Attribute: []Attribute{ + {Xmlns: "urn:oasis:names:tc:SAML:2.0:assertion", Name: "Email", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", FriendlyName: "E-Mail"}, + {Xmlns: "urn:oasis:names:tc:SAML:2.0:assertion", Name: "DisplayName", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", FriendlyName: "displayName"}, + {Xmlns: "urn:oasis:names:tc:SAML:2.0:assertion", Name: "Name", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", FriendlyName: "Name"}, + }, + SingleSignOnService: SingleSignOnService{ + Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", + Location: fmt.Sprintf("%s/login/saml/authorize/%s/%s", originFrontend, application.Owner, application.Name), + }, + ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol", + }, + } + + return &d, nil +} + +//GenerateSamlResponse generates a SAML response +func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, error) { + //decode samlRequest + defated, err := base64.StdEncoding.DecodeString(samlRequest) + if err != nil { + return "", "", fmt.Errorf("err: %s", err.Error()) + } + var buffer bytes.Buffer + rdr := flate.NewReader(bytes.NewReader(defated)) + io.Copy(&buffer, rdr) + var authnRequest saml.AuthnRequest + err = xml.Unmarshal(buffer.Bytes(), &authnRequest) + if err != nil { + return "", "", fmt.Errorf("err: %s", err.Error()) + } + //verify samlRequest + if valid := CheckRedirectUriValid(application, authnRequest.Issuer.Url); !valid { + return "", "", fmt.Errorf("err: invalid issuer url") + } + + //get publickey string + cert := getCertByApplication(application) + block, _ := pem.Decode([]byte(cert.PublicKey)) + publicKey := base64.StdEncoding.EncodeToString(block.Bytes) + + _, originBackend := getOriginFromHost(host) + + //build signedResponse + samlResponse, _ := NewSamlResponse(user, originBackend, publicKey, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, application.RedirectUris) + randomKeyStore := &X509Key{ + PrivateKey: cert.PrivateKey, + X509Certificate: publicKey, + } + ctx := dsig.NewDefaultSigningContext(randomKeyStore) + ctx.Hash = crypto.SHA1 + signedXML, err := ctx.SignEnveloped(samlResponse) + if err != nil { + return "", "", fmt.Errorf("err: %s", err.Error()) + } + + doc := etree.NewDocument() + doc.SetRoot(signedXML) + xmlStr, err := doc.WriteToString() + if err != nil { + return "", "", fmt.Errorf("err: %s", err.Error()) + } + res := base64.StdEncoding.EncodeToString([]byte(xmlStr)) + return res, authnRequest.AssertionConsumerServiceURL, nil +} diff --git a/object/saml.go b/object/saml_sp.go similarity index 100% rename from object/saml.go rename to object/saml_sp.go diff --git a/routers/router.go b/routers/router.go index 1b637ae9f4a6..184bafeef553 100644 --- a/routers/router.go +++ b/routers/router.go @@ -54,6 +54,7 @@ func initAPI() { 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/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta") 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 f8c192d53fc9..5f45b625d353 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -656,6 +656,7 @@ class App extends Component { this.renderHomeIfLoggedIn()}/> {this.onUpdateAccount(account)}} />}/> {this.onUpdateAccount(account)}} />}/> + {this.onUpdateAccount(account)}} />}/> this.renderHomeIfLoggedIn( this.setState({account: null})} {...props} />)} /> {return ()}} /> diff --git a/web/src/TokenEditPage.js b/web/src/TokenEditPage.js index 54926159ed5f..21a2fd19abca 100644 --- a/web/src/TokenEditPage.js +++ b/web/src/TokenEditPage.js @@ -135,7 +135,7 @@ class TokenEditPage extends React.Component { { - this.updateTokenField('expiresIn', e.target.value); + this.updateTokenField('expiresIn', parseInt(e.target.value)); }} /> diff --git a/web/src/auth/AuthCallback.js b/web/src/auth/AuthCallback.js index 2359c3afb15b..f9282e8400e2 100644 --- a/web/src/auth/AuthCallback.js +++ b/web/src/auth/AuthCallback.js @@ -49,6 +49,10 @@ class AuthCallback extends React.Component { const realRedirectUri = innerParams.get("redirect_uri"); // Casdoor's own login page, so "code" is not necessary if (realRedirectUri === null) { + const samlRequest = innerParams.get("SAMLRequest"); + if (samlRequest !== null && samlRequest !== undefined && samlRequest !== "") { + return "saml" + } return "login"; } @@ -92,6 +96,7 @@ class AuthCallback extends React.Component { const applicationName = innerParams.get("application"); const providerName = innerParams.get("provider"); const method = innerParams.get("method"); + const samlRequest = innerParams.get("SAMLRequest"); let redirectUri = `${window.location.origin}/callback`; @@ -100,6 +105,7 @@ class AuthCallback extends React.Component { application: applicationName, provider: providerName, code: code, + samlRequest: samlRequest, // state: innerParams.get("state"), state: applicationName, redirectUri: redirectUri, @@ -127,6 +133,10 @@ class AuthCallback extends React.Component { } else if (responseType === "link") { const from = innerParams.get("from"); Setting.goToLinkSoft(this, from); + } else if (responseType === "saml") { + const SAMLResponse = res.data; + const redirectUri = res.data2; + Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`); } } else { this.setState({ diff --git a/web/src/auth/LoginPage.js b/web/src/auth/LoginPage.js index ea2f3bbf2d4f..64a6cb7cf4ee 100644 --- a/web/src/auth/LoginPage.js +++ b/web/src/auth/LoginPage.js @@ -53,6 +53,7 @@ class LoginPage extends React.Component { classes: props, type: props.type, applicationName: props.applicationName !== undefined ? props.applicationName : (props.match === undefined ? null : props.match.params.applicationName), + owner : props.owner !== undefined ? props.owner : (props.match === undefined ? null : props.match.params.owner), application: null, mode: props.mode !== undefined ? props.mode : (props.match === undefined ? null : props.match.params.mode), // "signup" or "signin" isCodeSignin: false, @@ -61,7 +62,6 @@ class LoginPage extends React.Component { validEmailOrPhone: false, validEmail: false, validPhone: false, - owner: null, }; if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) { this.state.owner = props.match?.params.owner @@ -74,6 +74,8 @@ class LoginPage extends React.Component { this.getApplication(); } else if (this.state.type === "code") { this.getApplicationLogin(); + } else if (this.state.type === "saml"){ + this.getSamlApplication(); } else { Util.showMessage("error", `Unknown authentication type: ${this.state.type}`); } @@ -110,6 +112,19 @@ class LoginPage extends React.Component { }); } + getSamlApplication(){ + if (this.state.applicationName === null){ + return; + } + ApplicationBackend.getApplication(this.state.owner, this.state.applicationName) + .then((application) => { + this.setState({ + application: application, + }); + } + ); + } + getApplicationObj() { if (this.props.application !== undefined) { return this.props.application; @@ -157,7 +172,16 @@ class LoginPage extends React.Component { values["type"] = this.state.type; } values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix; + + if (oAuthParams !== null){ + values["samlRequest"] = oAuthParams.samlRequest; + } + + if (values["samlRequest"] != null && values["samlRequest"] !== "") { + values["type"] = "saml"; + } + AuthBackend.login(values, oAuthParams) .then((res) => { if (res.status === 'ok') { @@ -198,6 +222,10 @@ class LoginPage extends React.Component { } else if (responseType === "token" || responseType === "id_token") { const accessToken = res.data; Setting.goToLink(`${oAuthParams.redirectUri}#${responseType}=${accessToken}?state=${oAuthParams.state}&token_type=bearer`); + } else if (responseType === "saml") { + const SAMLResponse = res.data; + const redirectUri = res.data2; + Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`); } } else { Util.showMessage("error", `Failed to log in: ${res.msg}`); diff --git a/web/src/auth/Util.js b/web/src/auth/Util.js index 1f44acd0d3f2..3c171b4f8511 100644 --- a/web/src/auth/Util.js +++ b/web/src/auth/Util.js @@ -98,11 +98,13 @@ export function getOAuthGetParameters(params) { const redirectUri = getRefinedValue(queries.get("redirect_uri")); const scope = getRefinedValue(queries.get("scope")); const state = getRefinedValue(queries.get("state")); - const nonce = getRefinedValue(queries.get("nonce")) - const challengeMethod = getRefinedValue(queries.get("code_challenge_method")) - const codeChallenge = getRefinedValue(queries.get("code_challenge")) - - if (clientId === undefined || clientId === null || clientId === "") { + const nonce = getRefinedValue(queries.get("nonce")); + const challengeMethod = getRefinedValue(queries.get("code_challenge_method")); + const codeChallenge = getRefinedValue(queries.get("code_challenge")); + const samlRequest = getRefinedValue(queries.get("SAMLRequest")); + const relayState = getRefinedValue(queries.get("RelayState")); + + if ((clientId === undefined || clientId === null || clientId === "") && (samlRequest === "" || samlRequest === undefined)) { // login return null; } else { @@ -116,6 +118,8 @@ export function getOAuthGetParameters(params) { nonce: nonce, challengeMethod: challengeMethod, codeChallenge: codeChallenge, + samlRequest: samlRequest, + relayState: relayState, }; } }