diff --git a/controllers/auth.go b/controllers/auth.go index 5bf4fcb9..1f45d78a 100644 --- a/controllers/auth.go +++ b/controllers/auth.go @@ -477,11 +477,10 @@ func (c *ApiController) Login() { c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s is not enabled for the application"), provider.Name)) return } - userInfo := &idp.UserInfo{} if provider.Category == "SAML" { // SAML - userInfo.Id, err = object.ParseSamlResponse(authForm.SamlResponse, provider, c.Ctx.Request.Host) + userInfo, err = object.ParseSamlResponse(authForm.SamlResponse, provider, c.Ctx.Request.Host) if err != nil { c.ResponseError(err.Error()) return @@ -524,7 +523,8 @@ func (c *ApiController) Login() { if authForm.Method == "signup" { user := &object.User{} if provider.Category == "SAML" { - user, err = object.GetUser(util.GetId(application.Organization, userInfo.Id)) + // The userInfo.Id is the NameID in SAML response, it could be name / email / phone + user, err = object.GetUserByFields(application.Organization, userInfo.Id) if err != nil { c.ResponseError(err.Error()) return @@ -679,6 +679,7 @@ func (c *ApiController) Login() { record2.User = user.Name util.SafeGoroutine(func() { object.AddRecord(record2) }) } else if provider.Category == "SAML" { + // TODO: since we get the user info from SAML response, we can try to create the user resp = &Response{Status: "error", Msg: fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(application.Organization, userInfo.Id))} } // resp = &Response{Status: "ok", Msg: "", Data: res} diff --git a/object/saml_sp.go b/object/saml_sp.go index b9c3c622..cdca0f2a 100644 --- a/object/saml_sp.go +++ b/object/saml_sp.go @@ -23,23 +23,49 @@ import ( "regexp" "strings" + "github.com/casdoor/casdoor/idp" + "github.com/mitchellh/mapstructure" + "github.com/casdoor/casdoor/i18n" saml2 "github.com/russellhaering/gosaml2" dsig "github.com/russellhaering/goxmldsig" ) -func ParseSamlResponse(samlResponse string, provider *Provider, host string) (string, error) { +func ParseSamlResponse(samlResponse string, provider *Provider, host string) (*idp.UserInfo, error) { samlResponse, _ = url.QueryUnescape(samlResponse) sp, err := buildSp(provider, samlResponse, host) if err != nil { - return "", err + return nil, err } assertionInfo, err := sp.RetrieveAssertionInfo(samlResponse) if err != nil { - return "", err + return nil, err } - return assertionInfo.NameID, err + + userInfoMap := make(map[string]string) + for spAttr, idpAttr := range provider.UserMapping { + for _, attr := range assertionInfo.Values { + if attr.Name == idpAttr { + userInfoMap[spAttr] = attr.Values[0].Value + } + } + } + userInfoMap["id"] = assertionInfo.NameID + + customUserInfo := &idp.CustomUserInfo{} + err = mapstructure.Decode(userInfoMap, customUserInfo) + if err != nil { + return nil, err + } + userInfo := &idp.UserInfo{ + Id: customUserInfo.Id, + Username: customUserInfo.Username, + DisplayName: customUserInfo.DisplayName, + Email: customUserInfo.Email, + AvatarUrl: customUserInfo.AvatarUrl, + } + return userInfo, err } func GenerateSamlRequest(id, relayState, host, lang string) (auth string, method string, err error) { @@ -146,14 +172,24 @@ func getCertificateFromSamlResponse(samlResponse string, providerType string) (s if err != nil { return "", err } - - deStr := strings.Replace(string(de), "\n", "", -1) - tagMap := map[string]string{ - "Aliyun IDaaS": "ds", - "Keycloak": "dsig", - } + var ( + expression string + deStr = strings.Replace(string(de), "\n", "", -1) + tagMap = map[string]string{ + "Aliyun IDaaS": "ds", + "Keycloak": "dsig", + } + ) tag := tagMap[providerType] - expression := fmt.Sprintf("<%s:X509Certificate>([\\s\\S]*?)", tag, tag) + if tag == "" { + // ... + // ... + // ... + // ... + expression = "<[^>]*:?X509Certificate>([\\s\\S]*?)<[^>]*:?X509Certificate>" + } else { + expression = fmt.Sprintf("<%s:X509Certificate>([\\s\\S]*?)", tag, tag) + } res := regexp.MustCompile(expression).FindStringSubmatch(deStr) return res[1], nil } diff --git a/web/src/ProviderEditPage.js b/web/src/ProviderEditPage.js index 9f03ad93..86b3cf1c 100644 --- a/web/src/ProviderEditPage.js +++ b/web/src/ProviderEditPage.js @@ -379,10 +379,11 @@ class ProviderEditPage extends React.Component { loadSamlConfiguration() { const parser = new DOMParser(); - const xmlDoc = parser.parseFromString(this.state.provider.metadata, "text/xml"); - const cert = xmlDoc.getElementsByTagName("ds:X509Certificate")[0].childNodes[0].nodeValue; - const endpoint = xmlDoc.getElementsByTagName("md:SingleSignOnService")[0].getAttribute("Location"); - const issuerUrl = xmlDoc.getElementsByTagName("md:EntityDescriptor")[0].getAttribute("entityID"); + const rawXml = this.state.provider.metadata.replace("\n", ""); + const xmlDoc = parser.parseFromString(rawXml, "text/xml"); + const cert = xmlDoc.querySelector("X509Certificate").childNodes[0].nodeValue.replace(" ", ""); + const endpoint = xmlDoc.querySelector("SingleSignOnService").getAttribute("Location"); + const issuerUrl = xmlDoc.querySelector("EntityDescriptor").getAttribute("entityID"); this.updateProviderField("idP", cert); this.updateProviderField("endpoint", endpoint); this.updateProviderField("issuerUrl", issuerUrl); @@ -491,7 +492,7 @@ class ProviderEditPage extends React.Component { this.updateProviderField("type", value); if (value === "Local File System") { this.updateProviderField("domain", Setting.getFullServerUrl()); - } else if (value === "Custom") { + } else if (value === "Custom" && this.state.provider.category === "OAuth") { this.updateProviderField("customAuthUrl", "https://door.casdoor.com/login/oauth/authorize"); this.updateProviderField("scopes", "openid profile email"); this.updateProviderField("customTokenUrl", "https://door.casdoor.com/api/login/oauth/access_token"); @@ -553,48 +554,54 @@ class ProviderEditPage extends React.Component { ) } { - this.state.provider.type !== "Custom" ? null : ( + this.state.provider.type === "Custom" ? ( - - - {Setting.getLabel(i18next.t("provider:Auth URL"), i18next.t("provider:Auth URL - Tooltip"))} - - - { - this.updateProviderField("customAuthUrl", e.target.value); - }} /> - - - - - {Setting.getLabel(i18next.t("provider:Token URL"), i18next.t("provider:Token URL - Tooltip"))} - - - { - this.updateProviderField("customTokenUrl", e.target.value); - }} /> - - - - - {Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))} - - - { - this.updateProviderField("scopes", e.target.value); - }} /> - - - - - {Setting.getLabel(i18next.t("provider:UserInfo URL"), i18next.t("provider:UserInfo URL - Tooltip"))} - - - { - this.updateProviderField("customUserInfoUrl", e.target.value); - }} /> - - + { + this.state.provider.category === "OAuth" ? ( + + + + {Setting.getLabel(i18next.t("provider:Auth URL"), i18next.t("provider:Auth URL - Tooltip"))} + + + { + this.updateProviderField("customAuthUrl", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("provider:Token URL"), i18next.t("provider:Token URL - Tooltip"))} + + + { + this.updateProviderField("customTokenUrl", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))} + + + { + this.updateProviderField("scopes", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("provider:UserInfo URL"), i18next.t("provider:UserInfo URL - Tooltip"))} + + + { + this.updateProviderField("customUserInfoUrl", e.target.value); + }} /> + + + + ) : null + } {Setting.getLabel(i18next.t("provider:User mapping"), i18next.t("provider:User mapping - Tooltip"))} : @@ -631,7 +638,7 @@ class ProviderEditPage extends React.Component { - ) + ) : null } { (this.state.provider.category === "Captcha" && this.state.provider.type === "Default") || diff --git a/web/src/Setting.js b/web/src/Setting.js index b91be627..7b78792e 100644 --- a/web/src/Setting.js +++ b/web/src/Setting.js @@ -209,6 +209,10 @@ export const OtherProviderInfo = { logo: `${StaticBaseUrl}/img/social_keycloak.png`, url: "https://www.keycloak.org/", }, + "Custom": { + logo: `${StaticBaseUrl}/img/social_custom.png`, + url: "https://door.casdoor.com/", + }, }, Payment: { "Dummy": { @@ -866,10 +870,10 @@ export function getClickable(text) { } export function getProviderLogoURL(provider) { + if (provider.type === "Custom" && provider.customLogo) { + return provider.customLogo; + } if (provider.category === "OAuth") { - if (provider.type === "Custom" && provider.customLogo) { - return provider.customLogo; - } return `${StaticBaseUrl}/img/social_${provider.type.toLowerCase()}.png`; } else { const info = OtherProviderInfo[provider.category][provider.type]; @@ -1014,6 +1018,7 @@ export function getProviderTypeOptions(category) { return ([ {id: "Aliyun IDaaS", name: "Aliyun IDaaS"}, {id: "Keycloak", name: "Keycloak"}, + {id: "Custom", name: "Custom"}, ]); } else if (category === "Payment") { return ([