feat: support SAML POST binding (#2661)

* fix: support saml http post binding

* fix: support saml http post binding

* fix: support saml post binding sp
This commit is contained in:
dacongda 2024-02-01 17:28:56 +08:00 committed by GitHub
parent c4096788b2
commit ce0d45a70b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 94 additions and 35 deletions

View File

@ -80,6 +80,7 @@ p, *, *, *, /.well-known/jwks, *, *
p, *, *, GET, /api/get-saml-login, *, * p, *, *, GET, /api/get-saml-login, *, *
p, *, *, POST, /api/acs, *, * p, *, *, POST, /api/acs, *, *
p, *, *, GET, /api/saml/metadata, *, * p, *, *, GET, /api/saml/metadata, *, *
p, *, *, *, /api/saml/redirect, *, *
p, *, *, *, /cas, *, * p, *, *, *, /cas, *, *
p, *, *, *, /scim, *, * p, *, *, *, /scim, *, *
p, *, *, *, /api/webauthn, *, * p, *, *, *, /api/webauthn, *, *

View File

@ -912,7 +912,7 @@ func (c *ApiController) HandleSamlLogin() {
samlResponse = url.QueryEscape(samlResponse) samlResponse = url.QueryEscape(samlResponse)
targetUrl := fmt.Sprintf("%s?relayState=%s&samlResponse=%s", targetUrl := fmt.Sprintf("%s?relayState=%s&samlResponse=%s",
slice[4], relayState, samlResponse) slice[4], relayState, samlResponse)
c.Redirect(targetUrl, 303) c.Redirect(targetUrl, http.StatusSeeOther)
} }
// HandleOfficialAccountEvent ... // HandleOfficialAccountEvent ...

View File

@ -16,6 +16,7 @@ package controllers
import ( import (
"fmt" "fmt"
"net/http"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
) )
@ -34,7 +35,13 @@ func (c *ApiController) GetSamlMeta() {
return return
} }
metadata, err := object.GetSamlMeta(application, host) enablePostBinding, err := c.GetBool("enablePostBinding", false)
if err != nil {
c.ResponseError(err.Error())
return
}
metadata, err := object.GetSamlMeta(application, host, enablePostBinding)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -43,3 +50,17 @@ func (c *ApiController) GetSamlMeta() {
c.Data["xml"] = metadata c.Data["xml"] = metadata
c.ServeXML() c.ServeXML()
} }
func (c *ApiController) HandleSamlRedirect() {
host := c.Ctx.Request.Host
owner := c.Ctx.Input.Param(":owner")
application := c.Ctx.Input.Param(":application")
relayState := c.Input().Get("RelayState")
samlRequest := c.Input().Get("SAMLRequest")
targetURL := object.GetSamlRedirectAddress(owner, application, relayState, samlRequest, host)
c.Redirect(targetURL, http.StatusSeeOther)
}

View File

@ -52,31 +52,32 @@ type Application struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"` CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
Logo string `xorm:"varchar(200)" json:"logo"` Logo string `xorm:"varchar(200)" json:"logo"`
HomepageUrl string `xorm:"varchar(100)" json:"homepageUrl"` HomepageUrl string `xorm:"varchar(100)" json:"homepageUrl"`
Description string `xorm:"varchar(100)" json:"description"` Description string `xorm:"varchar(100)" json:"description"`
Organization string `xorm:"varchar(100)" json:"organization"` Organization string `xorm:"varchar(100)" json:"organization"`
Cert string `xorm:"varchar(100)" json:"cert"` Cert string `xorm:"varchar(100)" json:"cert"`
EnablePassword bool `json:"enablePassword"` EnablePassword bool `json:"enablePassword"`
EnableSignUp bool `json:"enableSignUp"` EnableSignUp bool `json:"enableSignUp"`
EnableSigninSession bool `json:"enableSigninSession"` EnableSigninSession bool `json:"enableSigninSession"`
EnableAutoSignin bool `json:"enableAutoSignin"` EnableAutoSignin bool `json:"enableAutoSignin"`
EnableCodeSignin bool `json:"enableCodeSignin"` EnableCodeSignin bool `json:"enableCodeSignin"`
EnableSamlCompress bool `json:"enableSamlCompress"` EnableSamlCompress bool `json:"enableSamlCompress"`
EnableSamlC14n10 bool `json:"enableSamlC14n10"` EnableSamlC14n10 bool `json:"enableSamlC14n10"`
EnableWebAuthn bool `json:"enableWebAuthn"` EnableSamlPostBinding bool `json:"enableSamlPostBinding"`
EnableLinkWithEmail bool `json:"enableLinkWithEmail"` EnableWebAuthn bool `json:"enableWebAuthn"`
OrgChoiceMode string `json:"orgChoiceMode"` EnableLinkWithEmail bool `json:"enableLinkWithEmail"`
SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"` OrgChoiceMode string `json:"orgChoiceMode"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"` SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"`
SigninMethods []*SigninMethod `xorm:"varchar(2000)" json:"signinMethods"` Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SignupItems []*SignupItem `xorm:"varchar(2000)" json:"signupItems"` SigninMethods []*SigninMethod `xorm:"varchar(2000)" json:"signinMethods"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"` SignupItems []*SignupItem `xorm:"varchar(2000)" json:"signupItems"`
OrganizationObj *Organization `xorm:"-" json:"organizationObj"` GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
CertPublicKey string `xorm:"-" json:"certPublicKey"` OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
Tags []string `xorm:"mediumtext" json:"tags"` CertPublicKey string `xorm:"-" json:"certPublicKey"`
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"` Tags []string `xorm:"mediumtext" json:"tags"`
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
ClientId string `xorm:"varchar(100)" json:"clientId"` ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"` ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`

Binary file not shown.

View File

@ -198,7 +198,7 @@ type Attribute struct {
Values []string `xml:"AttributeValue"` Values []string `xml:"AttributeValue"`
} }
func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) { func GetSamlMeta(application *Application, host string, enablePostBinding bool) (*IdpEntityDescriptor, error) {
cert, err := getCertByApplication(application) cert, err := getCertByApplication(application)
if err != nil { if err != nil {
return nil, err return nil, err
@ -217,6 +217,13 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
originFrontend, originBackend := getOriginFromHost(host) originFrontend, originBackend := getOriginFromHost(host)
idpLocation := ""
if enablePostBinding {
idpLocation = fmt.Sprintf("%s/api/saml/redirect/%s/%s", originBackend, application.Owner, application.Name)
} else {
idpLocation = fmt.Sprintf("%s/login/saml/authorize/%s/%s", originFrontend, application.Owner, application.Name)
}
d := IdpEntityDescriptor{ d := IdpEntityDescriptor{
XMLName: xml.Name{ XMLName: xml.Name{
Local: "md:EntityDescriptor", Local: "md:EntityDescriptor",
@ -248,7 +255,7 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
}, },
SingleSignOnService: SingleSignOnService{ SingleSignOnService: SingleSignOnService{
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", 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), Location: idpLocation,
}, },
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol", ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
}, },
@ -442,3 +449,8 @@ func NewSamlResponse11(user *User, requestID string, host string) *etree.Element
return samlResponse return samlResponse
} }
func GetSamlRedirectAddress(owner string, application string, relayState string, samlRequest string, host string) string {
originF, _ := getOriginFromHost(host)
return fmt.Sprintf("%s/login/saml/authorize/%s/%s?relayState=%s&samlRequest=%s", originF, owner, application, relayState, samlRequest)
}

View File

@ -164,6 +164,10 @@ func getUrlPath(urlPath string) string {
return "/api/webauthn" return "/api/webauthn"
} }
if strings.HasPrefix(urlPath, "/api/saml/redirect") {
return "/api/saml/redirect"
}
return urlPath return urlPath
} }

View File

@ -60,6 +60,7 @@ func initAPI() {
beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin") beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin")
beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin") beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin")
beego.Router("/api/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta") beego.Router("/api/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta")
beego.Router("/api/saml/redirect/:owner/:application", &controllers.ApiController{}, "*:HandleSamlRedirect")
beego.Router("/api/webhook", &controllers.ApiController{}, "POST:HandleOfficialAccountEvent") beego.Router("/api/webhook", &controllers.ApiController{}, "POST:HandleOfficialAccountEvent")
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType") beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus") beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")

View File

@ -5592,6 +5592,9 @@
"enableSamlCompress": { "enableSamlCompress": {
"type": "boolean" "type": "boolean"
}, },
"enableSamlPostBinding": {
"type": "boolean"
},
"enableSignUp": { "enableSignUp": {
"type": "boolean" "type": "boolean"
}, },

View File

@ -116,7 +116,6 @@ class ApplicationEditPage extends React.Component {
this.getApplication(); this.getApplication();
this.getOrganizations(); this.getOrganizations();
this.getProviders(); this.getProviders();
this.getSamlMetadata();
} }
getApplication() { getApplication() {
@ -146,6 +145,8 @@ class ApplicationEditPage extends React.Component {
}); });
this.getCerts(application.organization); this.getCerts(application.organization);
this.getSamlMetadata(application.enableSamlPostBinding);
}); });
} }
@ -186,8 +187,8 @@ class ApplicationEditPage extends React.Component {
}); });
} }
getSamlMetadata() { getSamlMetadata(checked) {
ApplicationBackend.getSamlMetadata("admin", this.state.applicationName) ApplicationBackend.getSamlMetadata("admin", this.state.applicationName, checked)
.then((data) => { .then((data) => {
this.setState({ this.setState({
samlMetadata: data, samlMetadata: data,
@ -663,6 +664,17 @@ class ApplicationEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Enable SAML POST binding"), i18next.t("application:Enable SAML POST binding - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.application.enableSamlPostBinding} onChange={checked => {
this.updateApplicationField("enableSamlPostBinding", checked);
this.getSamlMetadata(checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:SAML attributes"), i18next.t("general:SAML attributes - Tooltip"))} : {Setting.getLabel(i18next.t("general:SAML attributes"), i18next.t("general:SAML attributes - Tooltip"))} :
@ -688,7 +700,7 @@ class ApplicationEditPage extends React.Component {
/> />
<br /> <br />
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => { <Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}`); copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&post=${this.state.application.enableSamlPostBinding}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully")); Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}} }}
> >

View File

@ -89,8 +89,8 @@ export function deleteApplication(application) {
}).then(res => res.json()); }).then(res => res.json());
} }
export function getSamlMetadata(owner, name) { export function getSamlMetadata(owner, name, enablePostBinding) {
return fetch(`${Setting.ServerUrl}/api/saml/metadata?application=${owner}/${encodeURIComponent(name)}`, { return fetch(`${Setting.ServerUrl}/api/saml/metadata?application=${owner}/${encodeURIComponent(name)}&enablePostBinding=${enablePostBinding}`, {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@ -36,6 +36,8 @@
"Enable SAML C14N10 - Tooltip": "Use C14N10 instead of C14N11 in SAML", "Enable SAML C14N10 - Tooltip": "Use C14N10 instead of C14N11 in SAML",
"Enable SAML compression": "Enable SAML compression", "Enable SAML compression": "Enable SAML compression",
"Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp", "Enable SAML compression - Tooltip": "Whether to compress SAML response messages when Casdoor is used as SAML idp",
"Enable SAML POST binding": "Enable SAML POST binding",
"Enable SAML POST binding - Tooltip": "The HTTP POST binding uses input fields in a HTML form to send SAML messages, Enable when your SP use it",
"Enable side panel": "Enable side panel", "Enable side panel": "Enable side panel",
"Enable signin session - Tooltip": "Whether Casdoor maintains a session after logging into Casdoor from the application", "Enable signin session - Tooltip": "Whether Casdoor maintains a session after logging into Casdoor from the application",
"Enable signup": "Enable signup", "Enable signup": "Enable signup",

View File

@ -36,6 +36,8 @@
"Enable SAML C14N10 - Tooltip": "在SAML协议里使用C14N10而不是C14N11", "Enable SAML C14N10 - Tooltip": "在SAML协议里使用C14N10而不是C14N11",
"Enable SAML compression": "压缩SAML响应", "Enable SAML compression": "压缩SAML响应",
"Enable SAML compression - Tooltip": "Casdoor作为SAML IdP时是否压缩SAML响应信息", "Enable SAML compression - Tooltip": "Casdoor作为SAML IdP时是否压缩SAML响应信息",
"Enable SAML POST binding": "启用SAML POST Binding",
"Enable SAML POST binding - Tooltip": "HTTP POST绑定使用HTML表单中的输入字段发送SAML消息当SP使用它时启用",
"Enable side panel": "启用侧面板", "Enable side panel": "启用侧面板",
"Enable signin session - Tooltip": "从应用登录Casdoor后Casdoor是否保持会话", "Enable signin session - Tooltip": "从应用登录Casdoor后Casdoor是否保持会话",
"Enable signup": "启用注册", "Enable signup": "启用注册",