mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-04 05:10:19 +08:00
feat: implement CAS 3.0 (#659)
This commit is contained in:

committed by
GitHub

parent
15daf5dbfe
commit
7236cca8cf
@ -20,6 +20,7 @@ import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
@ -34,6 +35,7 @@ import (
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
//returns a saml2 response
|
||||
func NewSamlResponse(user *User, host string, publicKey string, destination string, iss string, redirectUri []string) (*etree.Element, error) {
|
||||
samlResponse := &etree.Element{
|
||||
Space: "samlp",
|
||||
@ -223,7 +225,8 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
//GenerateSamlResponse generates a SAML response
|
||||
//GenerateSamlResponse 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
|
||||
defated, err := base64.StdEncoding.DecodeString(samlRequest)
|
||||
@ -272,3 +275,78 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
res := base64.StdEncoding.EncodeToString([]byte(xmlStr))
|
||||
return res, authnRequest.AssertionConsumerServiceURL, nil
|
||||
}
|
||||
|
||||
//return a saml1.1 response(not 2.0)
|
||||
func NewSamlResponse11(user *User, requestID string, host string) *etree.Element {
|
||||
samlResponse := &etree.Element{
|
||||
Space: "samlp",
|
||||
Tag: "Response",
|
||||
}
|
||||
//create samlresponse
|
||||
samlResponse.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:1.0:protocol")
|
||||
samlResponse.CreateAttr("MajorVersion", "1")
|
||||
samlResponse.CreateAttr("MinorVersion", "1")
|
||||
|
||||
responseID := uuid.NewV4()
|
||||
samlResponse.CreateAttr("ResponseID", fmt.Sprintf("_%s", responseID))
|
||||
samlResponse.CreateAttr("InResponseTo", requestID)
|
||||
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
expireTime := time.Now().UTC().Add(time.Hour * 24).Format(time.RFC3339)
|
||||
|
||||
samlResponse.CreateAttr("IssueInstant", now)
|
||||
|
||||
samlResponse.CreateElement("samlp:Status").CreateElement("samlp:StatusCode").CreateAttr("Value", "samlp:Success")
|
||||
|
||||
//create assertion which is inside the response
|
||||
assertion := samlResponse.CreateElement("saml:Assertion")
|
||||
assertion.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:1.0:assertion")
|
||||
assertion.CreateAttr("MajorVersion", "1")
|
||||
assertion.CreateAttr("MinorVersion", "1")
|
||||
assertion.CreateAttr("AssertionID", uuid.NewV4().String())
|
||||
assertion.CreateAttr("Issuer", host)
|
||||
assertion.CreateAttr("IssueInstant", now)
|
||||
|
||||
condition := assertion.CreateElement("saml:Conditions")
|
||||
condition.CreateAttr("NotBefore", now)
|
||||
condition.CreateAttr("NotOnOrAfter", expireTime)
|
||||
|
||||
//AuthenticationStatement inside assertion
|
||||
authenticationStatement := assertion.CreateElement("saml:AuthenticationStatement")
|
||||
authenticationStatement.CreateAttr("AuthenticationMethod", "urn:oasis:names:tc:SAML:1.0:am:password")
|
||||
authenticationStatement.CreateAttr("AuthenticationInstant", now)
|
||||
|
||||
//subject inside AuthenticationStatement
|
||||
subject := assertion.CreateElement("saml:Subject")
|
||||
//nameIdentifier inside subject
|
||||
nameIdentifier := subject.CreateElement("saml:NameIdentifier")
|
||||
//nameIdentifier.CreateAttr("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")
|
||||
nameIdentifier.SetText(user.Name)
|
||||
|
||||
//subjectConfirmation inside subject
|
||||
subjectConfirmation := subject.CreateElement("saml:SubjectConfirmation")
|
||||
subjectConfirmation.CreateElement("saml:ConfirmationMethod").SetText("urn:oasis:names:tc:SAML:1.0:cm:artifact")
|
||||
|
||||
attributeStatement := assertion.CreateElement("saml:AttributeStatement")
|
||||
subjectInAttribute := attributeStatement.CreateElement("saml:Subject")
|
||||
nameIdentifierInAttribute := subjectInAttribute.CreateElement("saml:NameIdentifier")
|
||||
nameIdentifierInAttribute.SetText(user.Name)
|
||||
|
||||
subjectConfirmationInAttribute := subjectInAttribute.CreateElement("saml:SubjectConfirmation")
|
||||
subjectConfirmationInAttribute.CreateElement("saml:ConfirmationMethod").SetText("urn:oasis:names:tc:SAML:1.0:cm:artifact")
|
||||
|
||||
data, _ := json.Marshal(user)
|
||||
tmp := map[string]string{}
|
||||
json.Unmarshal(data, &tmp)
|
||||
|
||||
for k, v := range tmp {
|
||||
if v != "" {
|
||||
attr := attributeStatement.CreateElement("saml:Attribute")
|
||||
attr.CreateAttr("saml:AttributeName", k)
|
||||
attr.CreateAttr("saml:AttributeNamespace", "http://www.ja-sig.org/products/cas/")
|
||||
attr.CreateElement("saml:AttributeValue").SetText(v)
|
||||
}
|
||||
}
|
||||
|
||||
return samlResponse
|
||||
}
|
||||
|
@ -15,14 +15,19 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
dsig "github.com/russellhaering/goxmldsig"
|
||||
)
|
||||
|
||||
type CasServiceResponse struct {
|
||||
@ -84,6 +89,7 @@ type CasAnyAttribute struct {
|
||||
type CasAuthenticationSuccessWrapper struct {
|
||||
AuthenticationSuccess *CasAuthenticationSuccess // the token we issued
|
||||
Service string //to which service this token is issued
|
||||
UserId string
|
||||
}
|
||||
|
||||
type CasProxySuccess struct {
|
||||
@ -96,17 +102,32 @@ type CasProxyFailure struct {
|
||||
Message string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type Saml11Request struct {
|
||||
XMLName xml.Name `xml:"Request"`
|
||||
SAMLP string `xml:"samlp,attr"`
|
||||
MajorVersion string `xml:"MajorVersion,attr"`
|
||||
MinorVersion string `xml:"MinorVersion,attr"`
|
||||
RequestID string `xml:"RequestID,attr"`
|
||||
IssueInstant string `xml:"IssueInstance,attr"`
|
||||
AssertionArtifact Saml11AssertionArtifact
|
||||
}
|
||||
type Saml11AssertionArtifact struct {
|
||||
XMLName xml.Name `xml:"AssertionArtifact"`
|
||||
InnerXML string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
//st is short for service ticket
|
||||
var stToServiceResponse sync.Map
|
||||
|
||||
//pgt is short for proxy granting ticket
|
||||
var pgtToServiceResponse sync.Map
|
||||
|
||||
func StoreCasTokenForPgt(token *CasAuthenticationSuccess, service string) string {
|
||||
func StoreCasTokenForPgt(token *CasAuthenticationSuccess, service, userId string) string {
|
||||
pgt := fmt.Sprintf("PGT-%s", util.GenerateId())
|
||||
pgtToServiceResponse.Store(pgt, &CasAuthenticationSuccessWrapper{
|
||||
AuthenticationSuccess: token,
|
||||
Service: service,
|
||||
UserId: userId,
|
||||
})
|
||||
return pgt
|
||||
}
|
||||
@ -115,33 +136,45 @@ func GenerateId() {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func GetCasTokenByPgt(pgt string) (bool, *CasAuthenticationSuccess, string) {
|
||||
/**
|
||||
@ret1: whether a token is found
|
||||
@ret2: token, nil if not found
|
||||
@ret3: the service URL who requested to issue this token
|
||||
@ret4: userIf of user who requested to issue this token
|
||||
*/
|
||||
func GetCasTokenByPgt(pgt string) (bool, *CasAuthenticationSuccess, string, string) {
|
||||
if responseWrapperType, ok := pgtToServiceResponse.LoadAndDelete(pgt); ok {
|
||||
responseWrapperTypeCast := responseWrapperType.(*CasAuthenticationSuccessWrapper)
|
||||
return true, responseWrapperTypeCast.AuthenticationSuccess, responseWrapperTypeCast.Service
|
||||
return true, responseWrapperTypeCast.AuthenticationSuccess, responseWrapperTypeCast.Service, responseWrapperTypeCast.UserId
|
||||
}
|
||||
return false, nil, ""
|
||||
return false, nil, "", ""
|
||||
}
|
||||
|
||||
func GetCasTokenByTicket(ticket string) (bool, *CasAuthenticationSuccess, string) {
|
||||
/**
|
||||
@ret1: whether a token is found
|
||||
@ret2: token, nil if not found
|
||||
@ret3: the service URL who requested to issue this token
|
||||
@ret4: userIf of user who requested to issue this token
|
||||
*/
|
||||
func GetCasTokenByTicket(ticket string) (bool, *CasAuthenticationSuccess, string, string) {
|
||||
if responseWrapperType, ok := stToServiceResponse.LoadAndDelete(ticket); ok {
|
||||
responseWrapperTypeCast := responseWrapperType.(*CasAuthenticationSuccessWrapper)
|
||||
return true, responseWrapperTypeCast.AuthenticationSuccess, responseWrapperTypeCast.Service
|
||||
return true, responseWrapperTypeCast.AuthenticationSuccess, responseWrapperTypeCast.Service, responseWrapperTypeCast.UserId
|
||||
}
|
||||
return false, nil, ""
|
||||
return false, nil, "", ""
|
||||
}
|
||||
|
||||
func StoreCasTokenForProxyTicket(token *CasAuthenticationSuccess, targetService string) string {
|
||||
func StoreCasTokenForProxyTicket(token *CasAuthenticationSuccess, targetService, userId string) string {
|
||||
proxyTicket := fmt.Sprintf("PT-%s", util.GenerateId())
|
||||
stToServiceResponse.Store(proxyTicket, &CasAuthenticationSuccessWrapper{
|
||||
AuthenticationSuccess: token,
|
||||
Service: targetService,
|
||||
UserId: userId,
|
||||
})
|
||||
return proxyTicket
|
||||
}
|
||||
|
||||
func GenerateCasToken(userId string, service string) (string, error) {
|
||||
|
||||
if user := GetUser(userId); user != nil {
|
||||
authenticationSuccess := CasAuthenticationSuccess{
|
||||
User: user.Name,
|
||||
@ -166,11 +199,69 @@ func GenerateCasToken(userId string, service string) (string, error) {
|
||||
stToServiceResponse.Store(st, &CasAuthenticationSuccessWrapper{
|
||||
AuthenticationSuccess: &authenticationSuccess,
|
||||
Service: service,
|
||||
UserId: userId,
|
||||
})
|
||||
return st, nil
|
||||
} else {
|
||||
return "", fmt.Errorf("invalid user Id")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ret1: saml response
|
||||
@ret2: the service URL who requested to issue this token
|
||||
@ret3: error
|
||||
*/
|
||||
func GetValidationBySaml(samlRequest string, host string) (string, string, error) {
|
||||
var request Saml11Request
|
||||
err := xml.Unmarshal([]byte(samlRequest), &request)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
ticket := request.AssertionArtifact.InnerXML
|
||||
if ticket == "" {
|
||||
return "", "", fmt.Errorf("samlp:AssertionArtifact field not found")
|
||||
}
|
||||
|
||||
ok, _, service, userId := GetCasTokenByTicket(ticket)
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("ticket %s found", ticket)
|
||||
}
|
||||
|
||||
user := GetUser(userId)
|
||||
if user == nil {
|
||||
return "", "", fmt.Errorf("user %s found", userId)
|
||||
}
|
||||
application := GetApplicationByUser(user)
|
||||
if application == nil {
|
||||
return "", "", fmt.Errorf("application for user %s found", userId)
|
||||
}
|
||||
|
||||
samlResponse := NewSamlResponse11(user, request.RequestID, host)
|
||||
|
||||
cert := getCertByApplication(application)
|
||||
block, _ := pem.Decode([]byte(cert.PublicKey))
|
||||
publicKey := base64.StdEncoding.EncodeToString(block.Bytes)
|
||||
randomKeyStore := &X509Key{
|
||||
PrivateKey: cert.PrivateKey,
|
||||
X509Certificate: publicKey,
|
||||
}
|
||||
|
||||
ctx := dsig.NewDefaultSigningContext(randomKeyStore)
|
||||
ctx.Hash = crypto.SHA1
|
||||
signedXML, err := ctx.SignEnveloped(samlResponse)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("err: %s", err.Error())
|
||||
}
|
||||
|
||||
doc := etree.NewDocument()
|
||||
doc.SetRoot(signedXML)
|
||||
xmlStr, err := doc.WriteToString()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("err: %s", err.Error())
|
||||
}
|
||||
return xmlStr, service, nil
|
||||
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user