133 lines
3.6 KiB
Go
133 lines
3.6 KiB
Go
// Copyright 2026 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 (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/casdoor/casdoor/util"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
// LogoutTokenClaims represents the claims in an OIDC Back-Channel Logout token.
|
|
// See https://openid.net/specs/openid-connect-backchannel-1_0.html
|
|
type LogoutTokenClaims struct {
|
|
Events map[string]interface{} `json:"events"`
|
|
Sid string `json:"sid,omitempty"`
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
func generateLogoutToken(application *Application, user *User, sessionId string, host string) (string, error) {
|
|
nowTime := time.Now()
|
|
_, originBackend := getOriginFromHost(host)
|
|
|
|
events := map[string]interface{}{
|
|
"http://schemas.openid.net/event/backchannel-logout": map[string]interface{}{},
|
|
}
|
|
|
|
claims := LogoutTokenClaims{
|
|
Events: events,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
Issuer: originBackend,
|
|
Subject: user.Id,
|
|
Audience: []string{application.ClientId},
|
|
IssuedAt: jwt.NewNumericDate(nowTime),
|
|
ID: util.GetId(application.Owner, util.GenerateId()),
|
|
},
|
|
}
|
|
|
|
if sessionId != "" {
|
|
claims.Sid = sessionId
|
|
}
|
|
|
|
cert, err := getCertByApplication(application)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if cert == nil {
|
|
return "", fmt.Errorf("no cert found for application: %s", application.GetId())
|
|
}
|
|
|
|
var (
|
|
token *jwt.Token
|
|
key interface{}
|
|
)
|
|
|
|
signingMethod := application.TokenSigningMethod
|
|
if strings.Contains(signingMethod, "ES") {
|
|
token = jwt.NewWithClaims(jwt.SigningMethodES256, claims)
|
|
key, err = jwt.ParseECPrivateKeyFromPEM([]byte(cert.PrivateKey))
|
|
} else {
|
|
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
|
key, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(cert.PrivateKey))
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
token.Header["kid"] = cert.Name
|
|
return token.SignedString(key)
|
|
}
|
|
|
|
// SendBackchannelLogout sends OIDC Back-Channel Logout tokens to all registered
|
|
// backchannel_logout_uri endpoints for applications that have active tokens for the user.
|
|
// See https://openid.net/specs/openid-connect-backchannel-1_0.html
|
|
func SendBackchannelLogout(organization, username, sessionId, host string) {
|
|
tokens, err := GetActiveTokensByUser(organization, username)
|
|
if err != nil || len(tokens) == 0 {
|
|
return
|
|
}
|
|
|
|
// Deduplicate applications
|
|
seen := map[string]bool{}
|
|
for _, token := range tokens {
|
|
appId := util.GetId(token.Owner, token.Application)
|
|
if seen[appId] {
|
|
continue
|
|
}
|
|
seen[appId] = true
|
|
|
|
application, err := GetApplication(appId)
|
|
if err != nil || application == nil || application.BackchannelLogoutUri == "" {
|
|
continue
|
|
}
|
|
|
|
user, err := GetUser(util.GetId(organization, username))
|
|
if err != nil || user == nil {
|
|
continue
|
|
}
|
|
|
|
logoutToken, err := generateLogoutToken(application, user, sessionId, host)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
go postBackchannelLogout(application.BackchannelLogoutUri, logoutToken)
|
|
}
|
|
}
|
|
|
|
func postBackchannelLogout(logoutUri, logoutToken string) {
|
|
body := url.Values{"logout_token": {logoutToken}}
|
|
resp, err := http.PostForm(logoutUri, body)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
}
|