mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-03 12:30:19 +08:00
feat: support casdoor as saml idp to connect keycloak (#832)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
This commit is contained in:
@ -46,6 +46,7 @@ type Application struct {
|
||||
EnableSignUp bool `json:"enableSignUp"`
|
||||
EnableSigninSession bool `json:"enableSigninSession"`
|
||||
EnableCodeSignin bool `json:"enableCodeSignin"`
|
||||
EnableSamlCompress bool `json:"enableSamlCompress"`
|
||||
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
|
||||
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
|
||||
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
|
||||
|
@ -36,7 +36,7 @@ import (
|
||||
)
|
||||
|
||||
//returns a saml2 response
|
||||
func NewSamlResponse(user *User, host string, publicKey string, destination string, iss string, redirectUri []string) (*etree.Element, error) {
|
||||
func NewSamlResponse(user *User, host string, publicKey string, destination string, iss string, requestId string, redirectUri []string) (*etree.Element, error) {
|
||||
samlResponse := &etree.Element{
|
||||
Space: "samlp",
|
||||
Tag: "Response",
|
||||
@ -51,7 +51,7 @@ func NewSamlResponse(user *User, host string, publicKey string, destination stri
|
||||
samlResponse.CreateAttr("Version", "2.0")
|
||||
samlResponse.CreateAttr("IssueInstant", now)
|
||||
samlResponse.CreateAttr("Destination", destination)
|
||||
samlResponse.CreateAttr("InResponseTo", fmt.Sprintf("_%s", arId))
|
||||
samlResponse.CreateAttr("InResponseTo", requestId)
|
||||
samlResponse.CreateElement("saml:Issuer").SetText(host)
|
||||
|
||||
samlResponse.CreateElement("samlp:Status").CreateElement("samlp:StatusCode").CreateAttr("Value", "urn:oasis:names:tc:SAML:2.0:status:Success")
|
||||
@ -68,7 +68,7 @@ func NewSamlResponse(user *User, host string, publicKey string, destination stri
|
||||
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("InResponseTo", requestId)
|
||||
subjectConfirmationData.CreateAttr("Recipient", destination)
|
||||
subjectConfirmationData.CreateAttr("NotOnOrAfter", expireTime)
|
||||
condition := assertion.CreateElement("saml:Conditions")
|
||||
@ -225,14 +225,15 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
//GenerateSamlResponse generates a SAML2.0 response
|
||||
//parameter samlRequest is saml request in base64 format
|
||||
// GetSamlResponse generates a SAML2.0 response
|
||||
// parameter samlRequest is saml request in base64 format
|
||||
func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, error) {
|
||||
//decode samlRequest
|
||||
// base64 decode
|
||||
defated, err := base64.StdEncoding.DecodeString(samlRequest)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("err: %s", err.Error())
|
||||
}
|
||||
// decompress
|
||||
var buffer bytes.Buffer
|
||||
rdr := flate.NewReader(bytes.NewReader(defated))
|
||||
io.Copy(&buffer, rdr)
|
||||
@ -241,20 +242,21 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("err: %s", err.Error())
|
||||
}
|
||||
//verify samlRequest
|
||||
|
||||
// verify samlRequest
|
||||
if valid := CheckRedirectUriValid(application, authnRequest.Issuer.Url); !valid {
|
||||
return "", "", fmt.Errorf("err: invalid issuer url")
|
||||
}
|
||||
|
||||
//get publickey string
|
||||
// get public key 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)
|
||||
// build signedResponse
|
||||
samlResponse, _ := NewSamlResponse(user, originBackend, publicKey, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris)
|
||||
randomKeyStore := &X509Key{
|
||||
PrivateKey: cert.PrivateKey,
|
||||
X509Certificate: publicKey,
|
||||
@ -270,15 +272,28 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
|
||||
doc := etree.NewDocument()
|
||||
doc.SetRoot(samlResponse)
|
||||
xmlStr, err := doc.WriteToString()
|
||||
xmlBytes, err := doc.WriteToBytes()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("err: %s", err.Error())
|
||||
}
|
||||
res := base64.StdEncoding.EncodeToString([]byte(xmlStr))
|
||||
|
||||
// compress
|
||||
if application.EnableSamlCompress {
|
||||
flated := bytes.NewBuffer(nil)
|
||||
writer, err := flate.NewWriter(flated, flate.DefaultCompression)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("err: %s", err.Error())
|
||||
}
|
||||
writer.Write(xmlBytes)
|
||||
writer.Close()
|
||||
xmlBytes = flated.Bytes()
|
||||
}
|
||||
// base64 encode
|
||||
res := base64.StdEncoding.EncodeToString(xmlBytes)
|
||||
return res, authnRequest.AssertionConsumerServiceURL, nil
|
||||
}
|
||||
|
||||
//return a saml1.1 response(not 2.0)
|
||||
// NewSamlResponse11 return a saml1.1 response(not 2.0)
|
||||
func NewSamlResponse11(user *User, requestID string, host string) *etree.Element {
|
||||
samlResponse := &etree.Element{
|
||||
Space: "samlp",
|
||||
|
@ -474,6 +474,16 @@ class ApplicationEditPage extends React.Component {
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Enable SAML compress"), i18next.t("application:Enable SAML compress - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableSamlCompress} onChange={checked => {
|
||||
this.updateApplicationField('enableSamlCompress', checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:SAML metadata"), i18next.t("application:SAML metadata - Tooltip"))} :
|
||||
@ -484,6 +494,14 @@ class ApplicationEditPage extends React.Component {
|
||||
options={{mode: 'xml', theme: 'default'}}
|
||||
onBeforeChange={(editor, data, value) => {}}
|
||||
/>
|
||||
<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)}`);
|
||||
Setting.showMessage("success", i18next.t("application:SAML metadata URL copied to clipboard successfully"));
|
||||
}}
|
||||
>
|
||||
{i18next.t("application:Copy SAML metadata URL")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
|
@ -35,6 +35,7 @@ class ApplicationListPage extends BaseListPage {
|
||||
enableSignUp: true,
|
||||
enableSigninSession: false,
|
||||
enableCodeSignin: false,
|
||||
enableSamlCompress: false,
|
||||
providers: [
|
||||
{name: "provider_captcha_default", canSignUp: false, canSignIn: false, canUnlink: false, prompted: false, alertType: "None"},
|
||||
],
|
||||
|
@ -7,11 +7,14 @@
|
||||
},
|
||||
"application": {
|
||||
"Copy prompt page URL": "复制提醒页面URL",
|
||||
"Copy SAML metadata URL": "复制SAML元数据URL",
|
||||
"Copy signin page URL": "复制登录页面URL",
|
||||
"Copy signup page URL": "复制注册页面URL",
|
||||
"Edit Application": "编辑应用",
|
||||
"Enable code signin": "启用验证码登录",
|
||||
"Enable code signin - Tooltip": "是否允许用手机或邮箱验证码登录",
|
||||
"Enable SAML compress": "压缩SAML响应",
|
||||
"Enable SAML compress - Tooltip": "Casdoor作为SAML idp时,是否压缩SAML响应信息",
|
||||
"Enable signin session - Tooltip": "从应用登录Casdoor后,Casdoor是否保持会话",
|
||||
"Enable signup": "启用注册",
|
||||
"Enable signup - Tooltip": "是否允许用户注册",
|
||||
@ -30,6 +33,7 @@
|
||||
"Refresh token expire - Tooltip": "Refresh Token过期时间",
|
||||
"SAML metadata": "SAML元数据",
|
||||
"SAML metadata - Tooltip": "SAML协议的元数据(Metadata)信息",
|
||||
"SAML metadata URL copied to clipboard successfully": "SAML元数据URL已成功复制到剪贴板",
|
||||
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "登录页面URL已成功复制到剪贴板,请粘贴到当前浏览器的隐身模式窗口或另一个浏览器访问",
|
||||
"Signin session": "保持登录会话",
|
||||
"Signup items": "注册项",
|
||||
|
Reference in New Issue
Block a user