개요
Casdoor는 이제 SAML IdP로 사용될 수 있습니다. 지금까지, Casdoor는 SAML 2.0의 주요 기능을 지원하였습니다.
SP에서의 설정
일반적으로, SP는 세 가지 필수 필드를 요구합니다: Single Sign-On
, Issuer
, 그리고 Public Certificate
. 대부분의 SP는 XML 메타데이터 파일을 업로드하거나 XML 메타데이터 URL을 자동완성하여 이러한 필드를 얻을 수 있습니다.
Casdoor의 SAML 엔드포인트 메타데이터는 <Casdoor의 엔드포인트>/api/saml/metadata?application=admin/<어플리케이션 이름>
입니다. Casdoor의 엔드포인트가 https://door.casdoor.com
이고, app-built-in
이라는 어플리케이션이 포함되어 있다고 가정합시다. XML 메타데이터 엔드포인트는 다음과 같습니다:
https://door.casdoor.com/api/saml/metadata?application=admin/app-built-in
어플리케이션 편집 페이지에서 메타데이터를 찾을 수도 있습니다. 버튼을 클릭하여 URL을 복사하고 브라우저에 붙여넣어 XML 메타데이터를 다운로드합니다.
Casdoor IdP에서의 설정
Casdoor는 GET과 POST SAMLResponse
를 모두 지원합니다. Casdoor가 SAMLResponse
를 SP에게 보낼 때, Casdoor는 SP가 어떤 유형의 요청을 지원하는지 알아야 합니다. 당신은 Casdoor에서 애플리케이션을 구성해야 합니다. 이는 귀하의 SP가 지원하는 SAMLResponse
유형에 기반합니다.
Reply URL
을 입력하면, Casdoor는 POST 요청으로 SAMLResponse
를 보냅니다. 응답 URL이 비어 있으면, Casdoor는 GET 요청을 사용합니다. Casdoor가 Reply URL
이 비어 있을 경우 SP의 Reply URL
을 어떻게 알 수 있는지 궁금할 수 있습니다. 실제로, Casdoor는 SAMLRequest
를 파싱하여 AssertionConsumerServiceURL
이라는 URL을 얻고, SAMLResponse
를 가진 요청을 AssertionConsumerServiceURL
로 보낼 수 있습니다. Reply URL
은 SAMLRequest
의 AssertionConsumerServiceURL
을 덮어씁니다.
Reply URL: SAML 응답을 검증하는 ACS의 URL을 입력하세요.
Redirect URL: 고유한 이름을 입력하세요. 이것은 SP에서
Audience
또는Entity ID
로 불릴 수 있습니다. 당신의 SP에서와 같은Redirect URL
을 여기에 입력하세요.
사용자 프로필
성공적으로 로그인 한 후, Casdoor에서 반환된 SAMLResponse
의 사용자 프로필에는 세 개의 필드가 있습니다. XML의 속성과 Casdoor의 사용자 속성은 다음과 같이 매핑됩니다:
XML 속성 이름 | 사용자 필드 |
---|---|
이메일 | 이메일 |
DisplayName | displayName |
이름 | 이름 |
SAML 및 그 다양한 버전에 대한 자세한 정보는 https://en.wikipedia.org/wiki/SAML_2.0를 참조하세요.
예시
gosaml2는 etree와 goxmldsig를 기반으로 한 서비스 제공자를 위한 SAML 2.0 구현입니다. 이는 XML 디지털 서명의 순수 Go 구현입니다. 우리는 아래와 같이 Casdoor에서 SAML 2.0을 테스트하기 위해 이 라이브러리를 사용합니다.
http://localhost:7001/
을 통해 Casdoor에 접근할 수 있다고 가정하고, 귀하의 Casdoor에는 app-built-in
이라는 애플리케이션이 있습니다. 이는 built-in
이라는 조직에 속해 있습니다. URL들, http://localhost:6900/acs/example
와 http://localhost:6900/saml/acs/example
,은 app-built-in
의 Redirect URL에 추가되어야 합니다.
import (
"crypto/x509"
"fmt"
"net/http"
"io/ioutil"
"encoding/base64"
"encoding/xml"
saml2 "github.com/russellhaering/gosaml2"
"github.com/russellhaering/gosaml2/types"
dsig "github.com/russellhaering/goxmldsig"
)
func main() {
res, err := http.Get("http://localhost:7001/api/saml/metadata?application=admin/app-built-in")
if err != nil {
panic(err)
}
rawMetadata, err := ioutil.ReadAll(res.Body)
if err != nil {
panic(err)
}
metadata := &types.EntityDescriptor{}
err = xml.Unmarshal(rawMetadata, metadata)
if err != nil {
panic(err)
}
certStore := dsig.MemoryX509CertificateStore{
Roots: []*x509.Certificate{},
}
for _, kd := range metadata.IDPSSODescriptor.KeyDescriptors {
for idx, xcert := range kd.KeyInfo.X509Data.X509Certificates {
if xcert.Data == "" {
panic(fmt.Errorf("metadata certificate(%d) must not be empty", idx))
}
certData, err := base64.StdEncoding.DecodeString(xcert.Data)
if err != nil {
panic(err)
}
idpCert, err := x509.ParseCertificate(certData)
if err != nil {
panic(err)
}
certStore.Roots = append(certStore.Roots, idpCert)
}
}
randomKeyStore := dsig.RandomKeyStoreForTest()
sp := &saml2.SAMLServiceProvider{
IdentityProviderSSOURL: metadata.IDPSSODescriptor.SingleSignOnServices[0].Location,
IdentityProviderIssuer: metadata.EntityID,
ServiceProviderIssuer: "http://localhost:6900/acs/example",
AssertionConsumerServiceURL: "http://localhost:6900/v1/_saml_callback",
SignAuthnRequests: true,
AudienceURI: "http://localhost:6900/saml/acs/example",
IDPCertificateStore: &certStore,
SPKeyStore: randomKeyStore,
}
http.HandleFunc("/v1/_saml_callback", func(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
rw.WriteHeader(http.StatusBadRequest)
return
}
samlReponse := req.URL.Query().Get("SAMLResponse")
assertionInfo, err := sp.RetrieveAssertionInfo(samlReponse)
if err != nil {
fmt.Println(err)
rw.WriteHeader(http.StatusForbidden)
return
}
fmt.Println(assertionInfo)
if assertionInfo.WarningInfo.InvalidTime {
fmt.Println("here12:", assertionInfo.WarningInfo.InvalidTime)
rw.WriteHeader(http.StatusForbidden)
return
}
if assertionInfo.WarningInfo.NotInAudience {
fmt.Println(assertionInfo)
fmt.Println("here13:", assertionInfo.WarningInfo.NotInAudience)
rw.WriteHeader(http.StatusForbidden)
return
}
fmt.Fprintf(rw, "NameID: %s\n", assertionInfo.NameID)
fmt.Fprintf(rw, "Assertions:\n")
for key, val := range assertionInfo.Values {
fmt.Fprintf(rw, " %s: %+v\n", key, val)
}
fmt.Println(assertionInfo.Values.Get("FirstName"))
fmt.Fprintf(rw, "\n")
fmt.Fprintf(rw, "Warnings:\n")
fmt.Fprintf(rw, "%+v\n", assertionInfo.WarningInfo)
})
println("Visit this URL To Authenticate:")
authURL, err := sp.BuildAuthURL("")
if err != nil {
panic(err)
}
println(authURL)
println("Supply:")
fmt.Printf(" SP ACS URL : %s\n", sp.AssertionConsumerServiceURL)
err = http.ListenAndServe(":6900", nil)
if err != nil {
panic(err)
}
}
위의 코드를 실행하면, 콘솔에 다음과 같은 메시지가 표시됩니다.
Visit this URL To Authenticate:
http://localhost:7001/login/saml/authorize/admin/app-built-in?SAMLRequest=lFVbk6K8Fv0rFvNo2QR...
Supply:
SP ACS URL : http://localhost:6900/v1/_saml_callback
URL을 클릭하여 인증하면, Casdoor의 로그인 페이지가 표시됩니다.
인증 후, 아래와 같이 응답 메시지를 받게 됩니다.