mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-24 08:20:31 +08:00
feat: support CAS with organizations and applications (#621)
This commit is contained in:
parent
2023795f3c
commit
3cf1b990be
@ -101,6 +101,7 @@ p, *, *, GET, /.well-known/openid-configuration, *, *
|
||||
p, *, *, *, /.well-known/jwks, *, *
|
||||
p, *, *, GET, /api/get-saml-login, *, *
|
||||
p, *, *, POST, /api/acs, *, *
|
||||
p, *, *, *, /cas, *, *
|
||||
`
|
||||
|
||||
sa := stringadapter.NewAdapter(ruleText)
|
||||
|
@ -29,6 +29,7 @@ const (
|
||||
ResponseTypeCode = "code"
|
||||
ResponseTypeToken = "token"
|
||||
ResponseTypeIdToken = "id_token"
|
||||
ResponseTypeCas = "cas"
|
||||
)
|
||||
|
||||
type RequestForm struct {
|
||||
|
@ -83,8 +83,25 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
resp = tokenToResponse(token)
|
||||
}
|
||||
|
||||
} else if form.Type == ResponseTypeCas {
|
||||
//not oauth but CAS SSO protocol
|
||||
service := c.Input().Get("service")
|
||||
resp = wrapErrorResponse(nil)
|
||||
if service != "" {
|
||||
st, err := object.GenerateCasToken(userId, service)
|
||||
if err != nil {
|
||||
resp = wrapErrorResponse(err)
|
||||
} else {
|
||||
resp.Data = st
|
||||
}
|
||||
}
|
||||
if application.EnableSigninSession || application.HasPromptPage() {
|
||||
// The prompt page needs the user to be signed in
|
||||
c.SetSessionUsername(userId)
|
||||
}
|
||||
|
||||
} else {
|
||||
resp = &Response{Status: "error", Msg: fmt.Sprintf("Unknown response type: %s", form.Type)}
|
||||
resp = wrapErrorResponse(fmt.Errorf("Unknown response type: %s", form.Type))
|
||||
}
|
||||
|
||||
// if user did not check auto signin
|
||||
|
204
controllers/cas.go
Normal file
204
controllers/cas.go
Normal file
@ -0,0 +1,204 @@
|
||||
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
const (
|
||||
InvalidRequest string = "INVALID_REQUEST"
|
||||
InvalidTicketSpec string = "INVALID_TICKET_SPEC"
|
||||
UnauthorizedServiceProxy string = "UNAUTHORIZED_SERVICE_PROXY"
|
||||
InvalidProxyCallback string = "INVALID_PROXY_CALLBACK"
|
||||
InvalidTicket string = "INVALID_TICKET"
|
||||
InvalidService string = "INVALID_SERVICE"
|
||||
InteralError string = "INTERNAL_ERROR"
|
||||
UnauthorizedService string = "UNAUTHORIZED_SERVICE"
|
||||
)
|
||||
|
||||
func (c *RootController) CasValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
service := c.Input().Get("service")
|
||||
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
|
||||
if service == "" || ticket == "" {
|
||||
c.Ctx.Output.Body([]byte("no\n"))
|
||||
return
|
||||
}
|
||||
if ok, response, issuedService := object.GetCasTokenByTicket(ticket); ok {
|
||||
//check whether service is the one for which we previously issued token
|
||||
if issuedService == service {
|
||||
c.Ctx.Output.Body([]byte(fmt.Sprintf("yes\n%s\n", response.User)))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
//token not found
|
||||
c.Ctx.Output.Body([]byte("no\n"))
|
||||
}
|
||||
|
||||
func (c *RootController) CasServiceAndProxyValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
format := c.Input().Get("format")
|
||||
service := c.Input().Get("service")
|
||||
pgtUrl := c.Input().Get("pgtUrl")
|
||||
|
||||
serviceResponse := object.CasServiceResponse{
|
||||
Xmlns: "http://www.yale.edu/tp/cas",
|
||||
}
|
||||
|
||||
//check whether all required parameters are met
|
||||
if service == "" || ticket == "" {
|
||||
c.sendCasAuthenticationResponseErr(InvalidRequest, "service and ticket must exist", format)
|
||||
return
|
||||
}
|
||||
|
||||
//find the token
|
||||
if ok, response, issuedService := object.GetCasTokenByTicket(ticket); ok {
|
||||
|
||||
//check whether service is the one for which we previously issued token
|
||||
if strings.HasPrefix(service, issuedService) {
|
||||
serviceResponse.Success = response
|
||||
} else {
|
||||
//service not match
|
||||
c.sendCasAuthenticationResponseErr(InvalidService, fmt.Sprintf("service %s and %s does not match", service, issuedService), format)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
//token not found
|
||||
c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format)
|
||||
return
|
||||
}
|
||||
|
||||
if pgtUrl != "" && serviceResponse.Failure == nil {
|
||||
//that means we are in proxy web flow
|
||||
pgt := object.StoreCasTokenForPgt(serviceResponse.Success, service)
|
||||
pgtiou := serviceResponse.Success.ProxyGrantingTicket
|
||||
//todo: check whether it is https
|
||||
pgtUrlObj, err := url.Parse(pgtUrl)
|
||||
if pgtUrlObj.Scheme != "https" {
|
||||
c.sendCasAuthenticationResponseErr(InvalidProxyCallback, "callback is not https", format)
|
||||
return
|
||||
}
|
||||
//make a request to pgturl passing pgt and pgtiou
|
||||
if err != nil {
|
||||
c.sendCasAuthenticationResponseErr(InteralError, err.Error(), format)
|
||||
return
|
||||
}
|
||||
param := pgtUrlObj.Query()
|
||||
param.Add("pgtId", pgt)
|
||||
param.Add("pgtIou", pgtiou)
|
||||
pgtUrlObj.RawQuery = param.Encode()
|
||||
|
||||
request, err := http.NewRequest("GET", pgtUrlObj.String(), nil)
|
||||
if err != nil {
|
||||
c.sendCasAuthenticationResponseErr(InteralError, err.Error(), format)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(request)
|
||||
if err != nil || !(resp.StatusCode >= 200 && resp.StatusCode < 400) {
|
||||
//failed to send request
|
||||
c.sendCasAuthenticationResponseErr(InvalidProxyCallback, err.Error(), format)
|
||||
return
|
||||
}
|
||||
}
|
||||
// everything is ok, send the response
|
||||
if format == "json" {
|
||||
c.Data["json"] = serviceResponse
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
c.Data["xml"] = serviceResponse
|
||||
c.ServeXML()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RootController) CasProxy() {
|
||||
pgt := c.Input().Get("pgt")
|
||||
targetService := c.Input().Get("targetService")
|
||||
format := c.Input().Get("format")
|
||||
if pgt == "" || targetService == "" {
|
||||
c.sendCasProxyResponseErr(InvalidRequest, "pgt and targetService must exist", format)
|
||||
return
|
||||
}
|
||||
|
||||
ok, authenticationSuccess, issuedService := object.GetCasTokenByPgt(pgt)
|
||||
if !ok {
|
||||
c.sendCasProxyResponseErr(UnauthorizedService, "service not authorized", format)
|
||||
return
|
||||
}
|
||||
|
||||
newAuthenticationSuccess := authenticationSuccess.DeepCopy()
|
||||
if newAuthenticationSuccess.Proxies == nil {
|
||||
newAuthenticationSuccess.Proxies = &object.CasProxies{}
|
||||
}
|
||||
newAuthenticationSuccess.Proxies.Proxies = append(newAuthenticationSuccess.Proxies.Proxies, issuedService)
|
||||
proxyTicket := object.StoreCasTokenForProxyTicket(&newAuthenticationSuccess, targetService)
|
||||
|
||||
serviceResponse := object.CasServiceResponse{
|
||||
Xmlns: "http://www.yale.edu/tp/cas",
|
||||
ProxySuccess: &object.CasProxySuccess{
|
||||
ProxyTicket: proxyTicket,
|
||||
},
|
||||
}
|
||||
|
||||
if format == "json" {
|
||||
c.Data["json"] = serviceResponse
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
c.Data["xml"] = serviceResponse
|
||||
c.ServeXML()
|
||||
}
|
||||
|
||||
}
|
||||
func (c *RootController) sendCasProxyResponseErr(code, msg, format string) {
|
||||
serviceResponse := object.CasServiceResponse{
|
||||
Xmlns: "http://www.yale.edu/tp/cas",
|
||||
ProxyFailure: &object.CasProxyFailure{
|
||||
Code: code,
|
||||
Message: msg,
|
||||
},
|
||||
}
|
||||
if format == "json" {
|
||||
c.Data["json"] = serviceResponse
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
c.Data["xml"] = serviceResponse
|
||||
c.ServeXML()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RootController) sendCasAuthenticationResponseErr(code, msg, format string) {
|
||||
serviceResponse := object.CasServiceResponse{
|
||||
Xmlns: "http://www.yale.edu/tp/cas",
|
||||
Failure: &object.CasAuthenticationFailure{
|
||||
Code: code,
|
||||
Message: msg,
|
||||
},
|
||||
}
|
||||
|
||||
if format == "json" {
|
||||
c.Data["json"] = serviceResponse
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
c.Data["xml"] = serviceResponse
|
||||
c.ServeXML()
|
||||
}
|
||||
}
|
1
go.mod
1
go.mod
@ -25,7 +25,6 @@ require (
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/russellhaering/gosaml2 v0.6.0
|
||||
github.com/russellhaering/goxmldsig v1.1.1
|
||||
github.com/satori/go.uuid v1.2.0 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tealeg/xlsx v1.0.5
|
||||
|
2
go.sum
2
go.sum
@ -79,8 +79,6 @@ github.com/casbin/casbin/v2 v2.30.1 h1:P5HWadDL7olwUXNdcuKUBk+x75Y2eitFxYTcLNKeK
|
||||
github.com/casbin/casbin/v2 v2.30.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
|
||||
github.com/casbin/xorm-adapter/v2 v2.5.1 h1:BkpIxRHKa0s3bSMx173PpuU7oTs+Zw7XmD0BIta0HGM=
|
||||
github.com/casbin/xorm-adapter/v2 v2.5.1/go.mod h1:AeH4dBKHC9/zYxzdPVHhPDzF8LYLqjDdb767CWJoV54=
|
||||
github.com/casdoor/go-sms-sender v0.0.5 h1:9qhlMM+UoSOvvY7puUULqSHBBA7fbe02Px/tzchQboo=
|
||||
github.com/casdoor/go-sms-sender v0.0.5/go.mod h1:TMM/BsZQAa+7JVDXl2KqgxnzZgCjmHEX5MBN662mM5M=
|
||||
github.com/casdoor/go-sms-sender v0.2.0 h1:52bin4EBOPzOee64s9UK7jxd22FODvT9/+Y/Z+PSHpg=
|
||||
github.com/casdoor/go-sms-sender v0.2.0/go.mod h1:fsZsNnALvFIo+HFcE1U/oCQv4ZT42FdglXKMsEm3WSk=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
|
236
object/token_cas.go
Normal file
236
object/token_cas.go
Normal file
@ -0,0 +1,236 @@
|
||||
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
type CasServiceResponse struct {
|
||||
XMLName xml.Name `xml:"cas:serviceResponse" json:"-"`
|
||||
Xmlns string `xml:"xmlns:cas,attr"`
|
||||
Failure *CasAuthenticationFailure
|
||||
Success *CasAuthenticationSuccess
|
||||
ProxySuccess *CasProxySuccess
|
||||
ProxyFailure *CasProxyFailure
|
||||
}
|
||||
|
||||
type CasAuthenticationFailure struct {
|
||||
XMLName xml.Name `xml:"cas:authenticationFailure" json:"-"`
|
||||
Code string `xml:"code,attr"`
|
||||
Message string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type CasAuthenticationSuccess struct {
|
||||
XMLName xml.Name `xml:"cas:authenticationSuccess" json:"-"`
|
||||
User string `xml:"cas:user"`
|
||||
ProxyGrantingTicket string `xml:"cas:proxyGrantingTicket,omitempty"`
|
||||
Proxies *CasProxies `xml:"cas:proxies"`
|
||||
Attributes *CasAttributes `xml:"cas:attributes"`
|
||||
ExtraAttributes []*CasAnyAttribute `xml:",any"`
|
||||
}
|
||||
|
||||
type CasProxies struct {
|
||||
XMLName xml.Name `xml:"cas:proxies" json:"-"`
|
||||
Proxies []string `xml:"cas:proxy"`
|
||||
}
|
||||
|
||||
type CasAttributes struct {
|
||||
XMLName xml.Name `xml:"cas:attributes" json:"-"`
|
||||
AuthenticationDate time.Time `xml:"cas:authenticationDate"`
|
||||
LongTermAuthenticationRequestTokenUsed bool `xml:"cas:longTermAuthenticationRequestTokenUsed"`
|
||||
IsFromNewLogin bool `xml:"cas:isFromNewLogin"`
|
||||
MemberOf []string `xml:"cas:memberOf"`
|
||||
UserAttributes *CasUserAttributes
|
||||
ExtraAttributes []*CasAnyAttribute `xml:",any"`
|
||||
}
|
||||
|
||||
type CasUserAttributes struct {
|
||||
XMLName xml.Name `xml:"cas:userAttributes" json:"-"`
|
||||
Attributes []*CasNamedAttribute `xml:"cas:attribute"`
|
||||
AnyAttributes []*CasAnyAttribute `xml:",any"`
|
||||
}
|
||||
|
||||
type CasNamedAttribute struct {
|
||||
XMLName xml.Name `xml:"cas:attribute" json:"-"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
Value string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type CasAnyAttribute struct {
|
||||
XMLName xml.Name
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
type CasAuthenticationSuccessWrapper struct {
|
||||
AuthenticationSuccess *CasAuthenticationSuccess // the token we issued
|
||||
Service string //to which service this token is issued
|
||||
}
|
||||
|
||||
type CasProxySuccess struct {
|
||||
XMLName xml.Name `xml:"cas:proxySuccess" json:"-"`
|
||||
ProxyTicket string `xml:"cas:proxyTicket"`
|
||||
}
|
||||
type CasProxyFailure struct {
|
||||
XMLName xml.Name `xml:"cas:proxyFailure" json:"-"`
|
||||
Code string `xml:"code,attr"`
|
||||
Message 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 {
|
||||
pgt := fmt.Sprintf("PGT-%s", util.GenerateId())
|
||||
pgtToServiceResponse.Store(pgt, &CasAuthenticationSuccessWrapper{
|
||||
AuthenticationSuccess: token,
|
||||
Service: service,
|
||||
})
|
||||
return pgt
|
||||
}
|
||||
|
||||
func GenerateId() {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func GetCasTokenByPgt(pgt string) (bool, *CasAuthenticationSuccess, string) {
|
||||
if responseWrapperType, ok := pgtToServiceResponse.LoadAndDelete(pgt); ok {
|
||||
responseWrapperTypeCast := responseWrapperType.(*CasAuthenticationSuccessWrapper)
|
||||
return true, responseWrapperTypeCast.AuthenticationSuccess, responseWrapperTypeCast.Service
|
||||
}
|
||||
return false, nil, ""
|
||||
}
|
||||
|
||||
func GetCasTokenByTicket(ticket string) (bool, *CasAuthenticationSuccess, string) {
|
||||
if responseWrapperType, ok := stToServiceResponse.LoadAndDelete(ticket); ok {
|
||||
responseWrapperTypeCast := responseWrapperType.(*CasAuthenticationSuccessWrapper)
|
||||
return true, responseWrapperTypeCast.AuthenticationSuccess, responseWrapperTypeCast.Service
|
||||
}
|
||||
return false, nil, ""
|
||||
}
|
||||
|
||||
func StoreCasTokenForProxyTicket(token *CasAuthenticationSuccess, targetService string) string {
|
||||
proxyTicket := fmt.Sprintf("PT-%s", util.GenerateId())
|
||||
stToServiceResponse.Store(proxyTicket, &CasAuthenticationSuccessWrapper{
|
||||
AuthenticationSuccess: token,
|
||||
Service: targetService,
|
||||
})
|
||||
return proxyTicket
|
||||
}
|
||||
|
||||
func GenerateCasToken(userId string, service string) (string, error) {
|
||||
|
||||
if user := GetUser(userId); user != nil {
|
||||
authenticationSuccess := CasAuthenticationSuccess{
|
||||
User: user.Name,
|
||||
Attributes: &CasAttributes{
|
||||
AuthenticationDate: time.Now(),
|
||||
UserAttributes: &CasUserAttributes{},
|
||||
},
|
||||
ProxyGrantingTicket: fmt.Sprintf("PGTIOU-%s", util.GenerateId()),
|
||||
}
|
||||
data, _ := json.Marshal(user)
|
||||
tmp := map[string]string{}
|
||||
json.Unmarshal(data, &tmp)
|
||||
for k, v := range tmp {
|
||||
if v != "" {
|
||||
authenticationSuccess.Attributes.UserAttributes.Attributes = append(authenticationSuccess.Attributes.UserAttributes.Attributes, &CasNamedAttribute{
|
||||
Name: k,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
}
|
||||
st := fmt.Sprintf("ST-%d", rand.Int())
|
||||
stToServiceResponse.Store(st, &CasAuthenticationSuccessWrapper{
|
||||
AuthenticationSuccess: &authenticationSuccess,
|
||||
Service: service,
|
||||
})
|
||||
return st, nil
|
||||
} else {
|
||||
return "", fmt.Errorf("invalid user Id")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *CasAuthenticationSuccess) DeepCopy() CasAuthenticationSuccess {
|
||||
res := *c
|
||||
//copy proxy
|
||||
if c.Proxies != nil {
|
||||
tmp := c.Proxies.DeepCopy()
|
||||
res.Proxies = &tmp
|
||||
}
|
||||
if c.Attributes != nil {
|
||||
tmp := c.Attributes.DeepCopy()
|
||||
res.Attributes = &tmp
|
||||
}
|
||||
res.ExtraAttributes = make([]*CasAnyAttribute, len(c.ExtraAttributes))
|
||||
for i, e := range c.ExtraAttributes {
|
||||
tmp := *e
|
||||
res.ExtraAttributes[i] = &tmp
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (c *CasProxies) DeepCopy() CasProxies {
|
||||
res := CasProxies{
|
||||
Proxies: make([]string, len(c.Proxies)),
|
||||
}
|
||||
copy(res.Proxies, c.Proxies)
|
||||
return res
|
||||
}
|
||||
|
||||
func (c *CasAttributes) DeepCopy() CasAttributes {
|
||||
res := *c
|
||||
if c.MemberOf != nil {
|
||||
res.MemberOf = make([]string, len(c.MemberOf))
|
||||
copy(res.MemberOf, c.MemberOf)
|
||||
}
|
||||
tmp := c.UserAttributes.DeepCopy()
|
||||
res.UserAttributes = &tmp
|
||||
|
||||
res.ExtraAttributes = make([]*CasAnyAttribute, len(c.ExtraAttributes))
|
||||
for i, e := range c.ExtraAttributes {
|
||||
tmp := *e
|
||||
res.ExtraAttributes[i] = &tmp
|
||||
}
|
||||
return res
|
||||
|
||||
}
|
||||
|
||||
func (c *CasUserAttributes) DeepCopy() CasUserAttributes {
|
||||
res := CasUserAttributes{
|
||||
AnyAttributes: make([]*CasAnyAttribute, len(c.AnyAttributes)),
|
||||
Attributes: make([]*CasNamedAttribute, len(c.Attributes)),
|
||||
}
|
||||
for i, a := range c.AnyAttributes {
|
||||
var tmp = *a
|
||||
res.AnyAttributes[i] = &tmp
|
||||
}
|
||||
for i, a := range c.Attributes {
|
||||
var tmp = *a
|
||||
res.Attributes[i] = &tmp
|
||||
}
|
||||
return res
|
||||
}
|
@ -100,10 +100,17 @@ func willLog(subOwner string, subName string, method string, urlPath string, obj
|
||||
return true
|
||||
}
|
||||
|
||||
func getUrlPath(urlPath string) string {
|
||||
if strings.HasPrefix(urlPath, "/cas") && (strings.HasSuffix(urlPath, "/serviceValidate") || strings.HasSuffix(urlPath, "/proxy") || strings.HasSuffix(urlPath, "/proxyValidate") || strings.HasSuffix(urlPath, "/validate")) {
|
||||
return "/cas"
|
||||
}
|
||||
return urlPath
|
||||
}
|
||||
|
||||
func AuthzFilter(ctx *context.Context) {
|
||||
subOwner, subName := getSubject(ctx)
|
||||
method := ctx.Request.Method
|
||||
urlPath := ctx.Request.URL.Path
|
||||
urlPath := getUrlPath(ctx.Request.URL.Path)
|
||||
objOwner, objName := getObject(ctx)
|
||||
|
||||
isAllowed := authz.IsAllowed(subOwner, subName, method, urlPath, objOwner, objName)
|
||||
|
@ -171,4 +171,10 @@ func initAPI() {
|
||||
|
||||
beego.Router("/.well-known/openid-configuration", &controllers.RootController{}, "GET:GetOidcDiscovery")
|
||||
beego.Router("/.well-known/jwks", &controllers.RootController{}, "*:GetJwks")
|
||||
|
||||
beego.Router("/cas/:organization/:application/serviceValidate", &controllers.RootController{}, "GET:CasServiceAndProxyValidate")
|
||||
beego.Router("/cas/:organization/:application/proxyValidate", &controllers.RootController{}, "GET:CasServiceAndProxyValidate")
|
||||
beego.Router("/cas/:organization/:application/proxy", &controllers.RootController{}, "GET:CasProxy")
|
||||
beego.Router("/cas/:organization/:application/validate", &controllers.RootController{}, "GET:CasValidate")
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,9 @@ func StaticFilter(ctx *context.Context) {
|
||||
if strings.HasPrefix(urlPath, "/api/") || strings.HasPrefix(urlPath, "/.well-known/") {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(urlPath, "/cas") && (strings.HasSuffix(urlPath, "/serviceValidate") || strings.HasSuffix(urlPath, "/proxy") || strings.HasSuffix(urlPath, "/proxyValidate") || strings.HasSuffix(urlPath, "/validate")) {
|
||||
return
|
||||
}
|
||||
|
||||
path := "web/build"
|
||||
if urlPath == "/" {
|
||||
|
@ -18,6 +18,22 @@ module.exports = {
|
||||
'/.well-known/openid-configuration': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/cas/serviceValidate': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/cas/proxyValidate': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/cas/proxy': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/cas/validate': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -68,6 +68,7 @@ import i18next from 'i18next';
|
||||
import PromptPage from "./auth/PromptPage";
|
||||
import OdicDiscoveryPage from "./auth/OidcDiscoveryPage";
|
||||
import SamlCallback from './auth/SamlCallback';
|
||||
import CasLogout from "./auth/CasLogout";
|
||||
|
||||
const { Header, Footer } = Layout;
|
||||
|
||||
@ -642,7 +643,8 @@ class App extends Component {
|
||||
window.location.pathname.startsWith("/login") ||
|
||||
window.location.pathname.startsWith("/callback") ||
|
||||
window.location.pathname.startsWith("/prompt") ||
|
||||
window.location.pathname.startsWith("/forget");
|
||||
window.location.pathname.startsWith("/forget") ||
|
||||
window.location.pathname.startsWith("/cas");
|
||||
}
|
||||
|
||||
renderPage() {
|
||||
@ -654,6 +656,8 @@ class App extends Component {
|
||||
<Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/signup/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signup"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} />}/>
|
||||
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} />}/>
|
||||
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout clearAccount={() => this.setState({account: null})} {...props} />)} />
|
||||
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage type={"cas"} mode={"signup"} account={this.state.account} {...props} />)}} />
|
||||
<Route exact path="/callback" component={AuthCallback}/>
|
||||
<Route exact path="/callback/saml" component={SamlCallback}/>
|
||||
<Route exact path="/forget" render={(props) => this.renderHomeIfLoggedIn(<SelfForgetPage {...props} />)}/>
|
||||
|
@ -62,6 +62,14 @@ export function login(values, oAuthParams) {
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function loginCas(values, params) {
|
||||
return fetch(`${authConfig.serverUrl}/api/login?service=${params.service}`, {
|
||||
method: 'POST',
|
||||
credentials: "include",
|
||||
body: JSON.stringify(values),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return fetch(`${authConfig.serverUrl}/api/logout`, {
|
||||
method: 'POST',
|
||||
|
68
web/src/auth/CasLogout.js
Normal file
68
web/src/auth/CasLogout.js
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Spin} from "antd";
|
||||
import {withRouter} from "react-router-dom";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
class CasLogout extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
msg: null,
|
||||
};
|
||||
if (props.match?.params.casApplicationName !== undefined) {
|
||||
this.state.owner = props.match?.params.owner
|
||||
this.state.applicationName = props.match?.params.casApplicationName
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
const params = new URLSearchParams(this.props.location.search);
|
||||
|
||||
AuthBackend.logout()
|
||||
.then((res) => {
|
||||
if (res.status === 'ok') {
|
||||
Setting.showMessage("success", `Logged out successfully`);
|
||||
this.props.clearAccount()
|
||||
let redirectUri = res.data2;
|
||||
if (redirectUri !== null && redirectUri !== undefined && redirectUri !== "") {
|
||||
Setting.goToLink(redirectUri);
|
||||
} else if (params.has("service")) {
|
||||
Setting.goToLink(params.get("service"))
|
||||
} else {
|
||||
Setting.goToLinkSoft(this, `/cas/${this.state.owner}/${this.state.applicationName}/login`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to log out: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{textAlign: "center"}}>
|
||||
{
|
||||
<Spin size="large" tip={i18next.t("login:Logging out...")} style={{paddingTop: "10%"}} />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default withRouter(CasLogout);
|
@ -61,11 +61,16 @@ class LoginPage extends React.Component {
|
||||
validEmailOrPhone: false,
|
||||
validEmail: false,
|
||||
validPhone: false,
|
||||
owner: null,
|
||||
};
|
||||
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
|
||||
this.state.owner = props.match?.params.owner
|
||||
this.state.applicationName = props.match?.params.casApplicationName
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
if (this.state.type === "login") {
|
||||
if (this.state.type === "login" || this.state.type === "cas") {
|
||||
this.getApplication();
|
||||
} else if (this.state.type === "code") {
|
||||
this.getApplicationLogin();
|
||||
@ -120,59 +125,85 @@ class LoginPage extends React.Component {
|
||||
onFinish(values) {
|
||||
const application = this.getApplicationObj();
|
||||
const ths = this;
|
||||
const oAuthParams = Util.getOAuthGetParameters();
|
||||
if (oAuthParams !== null && oAuthParams.responseType!= null && oAuthParams.responseType !== "") {
|
||||
values["type"] = oAuthParams.responseType
|
||||
}else{
|
||||
|
||||
//here we are supposed to judge whether casdoor is working as a oauth server or CAS server
|
||||
if (this.state.type === "cas") {
|
||||
//cas
|
||||
const casParams = Util.getCasParameters()
|
||||
values["type"] = this.state.type;
|
||||
}
|
||||
values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix;
|
||||
|
||||
AuthBackend.login(values, oAuthParams)
|
||||
.then((res) => {
|
||||
AuthBackend.loginCas(values, casParams).then((res) => {
|
||||
if (res.status === 'ok') {
|
||||
const responseType = values["type"];
|
||||
if (responseType === "login") {
|
||||
Util.showMessage("success", `Logged in successfully`);
|
||||
|
||||
const link = Setting.getFromLink();
|
||||
Setting.goToLink(link);
|
||||
} else if (responseType === "code") {
|
||||
const code = res.data;
|
||||
const concatChar = oAuthParams?.redirectUri?.includes('?') ? '&' : '?';
|
||||
|
||||
if (Setting.hasPromptPage(application)) {
|
||||
AuthBackend.getAccount("")
|
||||
.then((res) => {
|
||||
let account = null;
|
||||
if (res.status === "ok") {
|
||||
account = res.data;
|
||||
account.organization = res.data2;
|
||||
|
||||
this.onUpdateAccount(account);
|
||||
|
||||
if (Setting.isPromptAnswered(account, application)) {
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
|
||||
} else {
|
||||
Setting.goToLinkSoft(ths, `/prompt/${application.name}?redirectUri=${oAuthParams.redirectUri}&code=${code}&state=${oAuthParams.state}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to sign in: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
|
||||
}
|
||||
|
||||
// Util.showMessage("success", `Authorization code: ${res.data}`);
|
||||
} else if (responseType === "token" || responseType === "id_token") {
|
||||
const accessToken = res.data;
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}#${responseType}=${accessToken}?state=${oAuthParams.state}&token_type=bearer`);
|
||||
let msg = "Logged in successfully. "
|
||||
if (casParams.service == "") {
|
||||
//If service was not specified, CAS MUST display a message notifying the client that it has successfully initiated a single sign-on session.
|
||||
msg += "Now you can visit apps protected by casdoor."
|
||||
}
|
||||
Util.showMessage("success", msg);
|
||||
if (casParams.service !== "") {
|
||||
let st = res.data
|
||||
window.location.href = casParams.service + "?ticket=" + st
|
||||
}
|
||||
|
||||
} else {
|
||||
Util.showMessage("error", `Failed to log in: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
})
|
||||
} else {
|
||||
//oauth
|
||||
const oAuthParams = Util.getOAuthGetParameters();
|
||||
if (oAuthParams !== null && oAuthParams.responseType != null && oAuthParams.responseType !== "") {
|
||||
values["type"] = oAuthParams.responseType
|
||||
}else{
|
||||
values["type"] = this.state.type;
|
||||
}
|
||||
values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix;
|
||||
|
||||
AuthBackend.login(values, oAuthParams)
|
||||
.then((res) => {
|
||||
if (res.status === 'ok') {
|
||||
const responseType = values["type"];
|
||||
if (responseType === "login") {
|
||||
Util.showMessage("success", `Logged in successfully`);
|
||||
|
||||
const link = Setting.getFromLink();
|
||||
Setting.goToLink(link);
|
||||
} else if (responseType === "code") {
|
||||
const code = res.data;
|
||||
const concatChar = oAuthParams?.redirectUri?.includes('?') ? '&' : '?';
|
||||
|
||||
if (Setting.hasPromptPage(application)) {
|
||||
AuthBackend.getAccount("")
|
||||
.then((res) => {
|
||||
let account = null;
|
||||
if (res.status === "ok") {
|
||||
account = res.data;
|
||||
account.organization = res.data2;
|
||||
|
||||
this.onUpdateAccount(account);
|
||||
|
||||
if (Setting.isPromptAnswered(account, application)) {
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
|
||||
} else {
|
||||
Setting.goToLinkSoft(ths, `/prompt/${application.name}?redirectUri=${oAuthParams.redirectUri}&code=${code}&state=${oAuthParams.state}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to sign in: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
|
||||
}
|
||||
|
||||
// Util.showMessage("success", `Authorization code: ${res.data}`);
|
||||
} else if (responseType === "token" || responseType === "id_token") {
|
||||
const accessToken = res.data;
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}#${responseType}=${accessToken}?state=${oAuthParams.state}&token_type=bearer`);
|
||||
}
|
||||
} else {
|
||||
Util.showMessage("error", `Failed to log in: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getSigninButton(type) {
|
||||
|
@ -79,6 +79,18 @@ function getRefinedValue(value){
|
||||
return (value === null)? "" : value
|
||||
}
|
||||
|
||||
export function getCasParameters(params){
|
||||
const queries = (params !== undefined) ? params : new URLSearchParams(window.location.search);
|
||||
const service = getRefinedValue(queries.get("service"))
|
||||
const renew = getRefinedValue(queries.get("renew"))
|
||||
const gateway = getRefinedValue(queries.get("gateway"))
|
||||
return {
|
||||
service: service,
|
||||
renew: renew,
|
||||
gateway: gateway,
|
||||
}
|
||||
}
|
||||
|
||||
export function getOAuthGetParameters(params) {
|
||||
const queries = (params !== undefined) ? params : new URLSearchParams(window.location.search);
|
||||
const clientId = getRefinedValue(queries.get("client_id"));
|
||||
|
Loading…
x
Reference in New Issue
Block a user