Compare commits

...

3 Commits

Author SHA1 Message Date
e593f5be5b fix: improve code format (#2665)
* feat: replace io/ioutils pacakage with io/os package

* fix: add missing error handling
2024-02-01 23:06:12 +08:00
0918757e85 feat: add template support for Custom HTTP SMS provider (#2662) 2024-02-01 17:50:22 +08:00
ce0d45a70b feat: support SAML POST binding (#2661)
* fix: support saml http post binding

* fix: support saml http post binding

* fix: support saml post binding sp
2024-02-01 17:28:56 +08:00
23 changed files with 130 additions and 48 deletions

View File

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

View File

@ -19,7 +19,7 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"io/ioutil"
"io"
"net/http"
"net/url"
"strconv"
@ -912,7 +912,7 @@ func (c *ApiController) HandleSamlLogin() {
samlResponse = url.QueryEscape(samlResponse)
targetUrl := fmt.Sprintf("%s?relayState=%s&samlResponse=%s",
slice[4], relayState, samlResponse)
c.Redirect(targetUrl, 303)
c.Redirect(targetUrl, http.StatusSeeOther)
}
// HandleOfficialAccountEvent ...
@ -921,7 +921,7 @@ func (c *ApiController) HandleSamlLogin() {
// @router /webhook [POST]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) HandleOfficialAccountEvent() {
respBytes, err := ioutil.ReadAll(c.Ctx.Request.Body)
respBytes, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.ResponseError(err.Error())
return

View File

@ -121,6 +121,10 @@ func (c *ApiController) Enforce() {
}
} else if owner != "" {
permissions, err = object.GetPermissions(owner)
if err != nil {
c.ResponseError(err.Error())
return
}
} else {
c.ResponseError(c.T("general:Missing parameter"))
return
@ -235,6 +239,10 @@ func (c *ApiController) BatchEnforce() {
}
} else if owner != "" {
permissions, err = object.GetPermissions(owner)
if err != nil {
c.ResponseError(err.Error())
return
}
} else {
c.ResponseError(c.T("general:Missing parameter"))
return

View File

@ -16,6 +16,7 @@ package controllers
import (
"fmt"
"net/http"
"github.com/casdoor/casdoor/object"
)
@ -34,7 +35,13 @@ func (c *ApiController) GetSamlMeta() {
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 {
c.ResponseError(err.Error())
return
@ -43,3 +50,17 @@ func (c *ApiController) GetSamlMeta() {
c.Data["xml"] = metadata
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

@ -47,6 +47,11 @@ func (c *ApiController) GetSystemInfo() {
// @router /get-version-info [get]
func (c *ApiController) GetVersionInfo() {
versionInfo, err := util.GetVersionInfo()
if err != nil {
c.ResponseError(err.Error())
return
}
if versionInfo.Version != "" {
c.ResponseOk(versionInfo)
return

View File

@ -20,7 +20,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
@ -218,7 +217,7 @@ func GetWechatOfficialAccountAccessToken(clientId string, clientSecret string) (
}
defer resp.Body.Close()
respBytes, err := ioutil.ReadAll(resp.Body)
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
@ -255,7 +254,7 @@ func GetWechatOfficialAccountQRCode(clientId string, clientSecret string) (strin
}
defer resp.Body.Close()
respBytes, err := ioutil.ReadAll(resp.Body)
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}

View File

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

Binary file not shown.

View File

@ -198,7 +198,7 @@ type Attribute struct {
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)
if err != nil {
return nil, err
@ -217,6 +217,13 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
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{
XMLName: xml.Name{
Local: "md:EntityDescriptor",
@ -248,7 +255,7 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
},
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),
Location: idpLocation,
},
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
}
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

@ -27,7 +27,7 @@ func getSmsClient(provider *Provider) (sender.SmsClient, error) {
if provider.Type == sender.HuaweiCloud || provider.Type == sender.AzureACS {
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.ProviderUrl, provider.AppId)
} else if provider.Type == "Custom HTTP SMS" {
client, err = newHttpSmsClient(provider.Endpoint, provider.Method, provider.Title)
client, err = newHttpSmsClient(provider.Endpoint, provider.Method, provider.Title, provider.TemplateCode)
} else {
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.AppId)
}

View File

@ -27,20 +27,26 @@ type HttpSmsClient struct {
endpoint string
method string
paramName string
template string
}
func newHttpSmsClient(endpoint string, method string, paramName string) (*HttpSmsClient, error) {
func newHttpSmsClient(endpoint, method, paramName, template string) (*HttpSmsClient, error) {
if template == "" {
template = "%s"
}
client := &HttpSmsClient{
endpoint: endpoint,
method: method,
paramName: paramName,
template: template,
}
return client, nil
}
func (c *HttpSmsClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
phoneNumber := targetPhoneNumber[0]
content := param["code"]
code := param["code"]
content := fmt.Sprintf(c.template, code)
var req *http.Request
var err error

View File

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

View File

@ -60,6 +60,7 @@ func initAPI() {
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/saml/redirect/:owner/:application", &controllers.ApiController{}, "*:HandleSamlRedirect")
beego.Router("/api/webhook", &controllers.ApiController{}, "POST:HandleOfficialAccountEvent")
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")

View File

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

View File

@ -16,7 +16,6 @@ package util
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
@ -40,7 +39,7 @@ func GetPath(path string) string {
func ListFiles(path string) []string {
res := []string{}
files, err := ioutil.ReadDir(path)
files, err := os.ReadDir(path)
if err != nil {
panic(err)
}

View File

@ -14,10 +14,10 @@
package util
import "io/ioutil"
import "os"
func GetUploadXlsxPath(fileId string) string {
file, err := ioutil.TempFile("", fileId)
file, err := os.CreateTemp("", fileId)
if err != nil {
panic(err)
}

View File

@ -119,6 +119,9 @@ func GetVersionInfo() (*VersionInfo, error) {
}
cIter, err := r.Log(&git.LogOptions{From: ref.Hash()})
if err != nil {
return res, err
}
commitOffset := 0
version := ""

View File

@ -70,6 +70,9 @@ func TestGetVersion(t *testing.T) {
testHash := plumbing.NewHash("f8bc87eb4e5ba3256424cf14aafe0549f812f1cf")
cIter, err := r.Log(&git.LogOptions{From: testHash})
if err != nil {
t.Log(err)
}
aheadCnt := 0
releaseVersion := ""

View File

@ -116,7 +116,6 @@ class ApplicationEditPage extends React.Component {
this.getApplication();
this.getOrganizations();
this.getProviders();
this.getSamlMetadata();
}
getApplication() {
@ -146,6 +145,8 @@ class ApplicationEditPage extends React.Component {
});
this.getCerts(application.organization);
this.getSamlMetadata(application.enableSamlPostBinding);
});
}
@ -186,8 +187,8 @@ class ApplicationEditPage extends React.Component {
});
}
getSamlMetadata() {
ApplicationBackend.getSamlMetadata("admin", this.state.applicationName)
getSamlMetadata(checked) {
ApplicationBackend.getSamlMetadata("admin", this.state.applicationName, checked)
.then((data) => {
this.setState({
samlMetadata: data,
@ -663,6 +664,17 @@ class ApplicationEditPage extends React.Component {
}} />
</Col>
</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"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:SAML attributes"), i18next.t("general:SAML attributes - Tooltip"))} :
@ -688,7 +700,7 @@ class ApplicationEditPage extends React.Component {
/>
<br />
<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"));
}}
>

View File

@ -1041,7 +1041,7 @@ class ProviderEditPage extends React.Component {
</Row>
)
}
{["Custom HTTP SMS", "Infobip SMS"].includes(this.state.provider.type) ?
{["Infobip SMS"].includes(this.state.provider.type) ?
null :
(<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>

View File

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

View File

@ -36,6 +36,8 @@
"Enable SAML C14N10 - Tooltip": "Use C14N10 instead of C14N11 in SAML",
"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 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 signin session - Tooltip": "Whether Casdoor maintains a session after logging into Casdoor from the application",
"Enable signup": "Enable signup",

View File

@ -36,6 +36,8 @@
"Enable SAML C14N10 - Tooltip": "在SAML协议里使用C14N10而不是C14N11",
"Enable SAML compression": "压缩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 signin session - Tooltip": "从应用登录Casdoor后Casdoor是否保持会话",
"Enable signup": "启用注册",