github.com/vnforks/kid/v5@v5.22.1-0.20200408055009-b89d99c65676/app/saml.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package app 5 6 import ( 7 "crypto/x509" 8 "encoding/pem" 9 "encoding/xml" 10 "fmt" 11 "io/ioutil" 12 "mime/multipart" 13 "net/http" 14 "strings" 15 16 "github.com/vnforks/kid/v5/model" 17 ) 18 19 const ( 20 SamlPublicCertificateName = "saml-public.crt" 21 SamlPrivateKeyName = "saml-private.key" 22 SamlIdpCertificateName = "saml-idp.crt" 23 ) 24 25 func (a *App) GetSamlMetadata() (string, *model.AppError) { 26 if a.Saml() == nil { 27 err := model.NewAppError("GetSamlMetadata", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented) 28 return "", err 29 } 30 31 result, err := a.Saml().GetMetadata() 32 if err != nil { 33 return "", model.NewAppError("GetSamlMetadata", "api.admin.saml.metadata.app_error", nil, "err="+err.Message, err.StatusCode) 34 } 35 return result, nil 36 } 37 38 func (a *App) writeSamlFile(filename string, fileData *multipart.FileHeader) *model.AppError { 39 file, err := fileData.Open() 40 if err != nil { 41 return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.open.app_error", nil, err.Error(), http.StatusInternalServerError) 42 } 43 defer file.Close() 44 45 data, err := ioutil.ReadAll(file) 46 if err != nil { 47 return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error(), http.StatusInternalServerError) 48 } 49 50 err = a.Srv().configStore.SetFile(filename, data) 51 if err != nil { 52 return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error(), http.StatusInternalServerError) 53 } 54 55 return nil 56 } 57 58 func (a *App) AddSamlPublicCertificate(fileData *multipart.FileHeader) *model.AppError { 59 if err := a.writeSamlFile(SamlPublicCertificateName, fileData); err != nil { 60 return err 61 } 62 63 cfg := a.Config().Clone() 64 *cfg.SamlSettings.PublicCertificateFile = SamlPublicCertificateName 65 66 if err := cfg.IsValid(); err != nil { 67 return err 68 } 69 70 a.UpdateConfig(func(dest *model.Config) { *dest = *cfg }) 71 72 return nil 73 } 74 75 func (a *App) AddSamlPrivateCertificate(fileData *multipart.FileHeader) *model.AppError { 76 if err := a.writeSamlFile(SamlPrivateKeyName, fileData); err != nil { 77 return err 78 } 79 80 cfg := a.Config().Clone() 81 *cfg.SamlSettings.PrivateKeyFile = SamlPrivateKeyName 82 83 if err := cfg.IsValid(); err != nil { 84 return err 85 } 86 87 a.UpdateConfig(func(dest *model.Config) { *dest = *cfg }) 88 89 return nil 90 } 91 92 func (a *App) AddSamlIdpCertificate(fileData *multipart.FileHeader) *model.AppError { 93 if err := a.writeSamlFile(SamlIdpCertificateName, fileData); err != nil { 94 return err 95 } 96 97 cfg := a.Config().Clone() 98 *cfg.SamlSettings.IdpCertificateFile = SamlIdpCertificateName 99 100 if err := cfg.IsValid(); err != nil { 101 return err 102 } 103 104 a.UpdateConfig(func(dest *model.Config) { *dest = *cfg }) 105 106 return nil 107 } 108 109 func (a *App) removeSamlFile(filename string) *model.AppError { 110 if err := a.Srv().configStore.RemoveFile(filename); err != nil { 111 return model.NewAppError("RemoveSamlFile", "api.admin.remove_certificate.delete.app_error", map[string]interface{}{"Filename": filename}, err.Error(), http.StatusInternalServerError) 112 } 113 114 return nil 115 } 116 117 func (a *App) RemoveSamlPublicCertificate() *model.AppError { 118 if err := a.removeSamlFile(*a.Config().SamlSettings.PublicCertificateFile); err != nil { 119 return err 120 } 121 122 cfg := a.Config().Clone() 123 *cfg.SamlSettings.PublicCertificateFile = "" 124 *cfg.SamlSettings.Encrypt = false 125 126 if err := cfg.IsValid(); err != nil { 127 return err 128 } 129 130 a.UpdateConfig(func(dest *model.Config) { *dest = *cfg }) 131 132 return nil 133 } 134 135 func (a *App) RemoveSamlPrivateCertificate() *model.AppError { 136 if err := a.removeSamlFile(*a.Config().SamlSettings.PrivateKeyFile); err != nil { 137 return err 138 } 139 140 cfg := a.Config().Clone() 141 *cfg.SamlSettings.PrivateKeyFile = "" 142 *cfg.SamlSettings.Encrypt = false 143 144 if err := cfg.IsValid(); err != nil { 145 return err 146 } 147 148 a.UpdateConfig(func(dest *model.Config) { *dest = *cfg }) 149 150 return nil 151 } 152 153 func (a *App) RemoveSamlIdpCertificate() *model.AppError { 154 if err := a.removeSamlFile(*a.Config().SamlSettings.IdpCertificateFile); err != nil { 155 return err 156 } 157 158 cfg := a.Config().Clone() 159 *cfg.SamlSettings.IdpCertificateFile = "" 160 *cfg.SamlSettings.Enable = false 161 162 if err := cfg.IsValid(); err != nil { 163 return err 164 } 165 166 a.UpdateConfig(func(dest *model.Config) { *dest = *cfg }) 167 168 return nil 169 } 170 171 func (a *App) GetSamlCertificateStatus() *model.SamlCertificateStatus { 172 status := &model.SamlCertificateStatus{} 173 174 status.IdpCertificateFile, _ = a.Srv().configStore.HasFile(*a.Config().SamlSettings.IdpCertificateFile) 175 status.PrivateKeyFile, _ = a.Srv().configStore.HasFile(*a.Config().SamlSettings.PrivateKeyFile) 176 status.PublicCertificateFile, _ = a.Srv().configStore.HasFile(*a.Config().SamlSettings.PublicCertificateFile) 177 178 return status 179 } 180 181 func (a *App) GetSamlMetadataFromIdp(idpMetadataUrl string) (*model.SamlMetadataResponse, *model.AppError) { 182 if a.Saml() == nil { 183 err := model.NewAppError("GetSamlMetadataFromIdp", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented) 184 return nil, err 185 } 186 187 if !strings.HasPrefix(idpMetadataUrl, "http://") && !strings.HasPrefix(idpMetadataUrl, "https://") { 188 idpMetadataUrl = "https://" + idpMetadataUrl 189 } 190 191 idpMetadataRaw, err := a.FetchSamlMetadataFromIdp(idpMetadataUrl) 192 if err != nil { 193 return nil, err 194 } 195 196 data, err := a.BuildSamlMetadataObject(idpMetadataRaw) 197 if err != nil { 198 return nil, err 199 } 200 201 return data, nil 202 } 203 204 func (a *App) FetchSamlMetadataFromIdp(url string) ([]byte, *model.AppError) { 205 resp, err := a.HTTPService().MakeClient(false).Get(url) 206 if err != nil { 207 return nil, model.NewAppError("FetchSamlMetadataFromIdp", "app.admin.saml.invalid_response_from_idp.app_error", nil, err.Error(), http.StatusBadRequest) 208 } 209 210 if resp.StatusCode != http.StatusOK { 211 return nil, model.NewAppError("FetchSamlMetadataFromIdp", "app.admin.saml.invalid_response_from_idp.app_error", nil, fmt.Sprintf("status_code=%d", resp.StatusCode), http.StatusBadRequest) 212 } 213 defer resp.Body.Close() 214 215 bodyXML, err := ioutil.ReadAll(resp.Body) 216 if err != nil { 217 return nil, model.NewAppError("FetchSamlMetadataFromIdp", "app.admin.saml.failure_read_response_body_from_idp.app_error", nil, err.Error(), http.StatusInternalServerError) 218 } 219 220 return bodyXML, nil 221 } 222 223 func (a *App) BuildSamlMetadataObject(idpMetadata []byte) (*model.SamlMetadataResponse, *model.AppError) { 224 entityDescriptor := model.EntityDescriptor{} 225 err := xml.Unmarshal(idpMetadata, &entityDescriptor) 226 if err != nil { 227 return nil, model.NewAppError("BuildSamlMetadataObject", "app.admin.saml.failure_decode_metadata_xml_from_idp.app_error", nil, err.Error(), http.StatusInternalServerError) 228 } 229 230 data := &model.SamlMetadataResponse{} 231 data.IdpDescriptorUrl = entityDescriptor.EntityID 232 233 if entityDescriptor.IDPSSODescriptors == nil || len(entityDescriptor.IDPSSODescriptors) == 0 { 234 err := model.NewAppError("BuildSamlMetadataObject", "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", nil, "", http.StatusInternalServerError) 235 return nil, err 236 } 237 238 idpSSODescriptor := entityDescriptor.IDPSSODescriptors[0] 239 if idpSSODescriptor.SingleSignOnServices == nil || len(idpSSODescriptor.SingleSignOnServices) == 0 { 240 err := model.NewAppError("BuildSamlMetadataObject", "api.admin.saml.invalid_xml_missing_ssoservices.app_error", nil, "", http.StatusInternalServerError) 241 return nil, err 242 } 243 244 data.IdpUrl = idpSSODescriptor.SingleSignOnServices[0].Location 245 if idpSSODescriptor.SSODescriptor.RoleDescriptor.KeyDescriptors == nil || len(idpSSODescriptor.SSODescriptor.RoleDescriptor.KeyDescriptors) == 0 { 246 err := model.NewAppError("BuildSamlMetadataObject", "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", nil, "", http.StatusInternalServerError) 247 return nil, err 248 } 249 keyDescriptor := idpSSODescriptor.SSODescriptor.RoleDescriptor.KeyDescriptors[0] 250 data.IdpPublicCertificate = keyDescriptor.KeyInfo.X509Data.X509Certificate.Cert 251 252 return data, nil 253 } 254 255 func (a *App) SetSamlIdpCertificateFromMetadata(data []byte) *model.AppError { 256 const certPrefix = "-----BEGIN CERTIFICATE-----\n" 257 const certSuffix = "\n-----END CERTIFICATE-----" 258 fixedCertTxt := certPrefix + string(data) + certSuffix 259 260 block, _ := pem.Decode([]byte(fixedCertTxt)) 261 if _, e := x509.ParseCertificate(block.Bytes); e != nil { 262 return model.NewAppError("SetSamlIdpCertificateFromMetadata", "api.admin.saml.failure_parse_idp_certificate.app_error", nil, e.Error(), http.StatusInternalServerError) 263 } 264 265 data = pem.EncodeToMemory(&pem.Block{ 266 Type: "CERTIFICATE", 267 Bytes: block.Bytes, 268 }) 269 270 if err := a.Srv().configStore.SetFile(SamlIdpCertificateName, data); err != nil { 271 return model.NewAppError("SetSamlIdpCertificateFromMetadata", "api.admin.saml.failure_save_idp_certificate_file.app_error", nil, err.Error(), http.StatusInternalServerError) 272 } 273 274 cfg := a.Config().Clone() 275 *cfg.SamlSettings.IdpCertificateFile = SamlIdpCertificateName 276 277 if err := cfg.IsValid(); err != nil { 278 return err 279 } 280 281 a.UpdateConfig(func(dest *model.Config) { *dest = *cfg }) 282 283 return nil 284 }