forked from casdoor/casdoor
197 lines
6.4 KiB
Go
197 lines
6.4 KiB
Go
// Copyright 2023 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 (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/casdoor/casdoor/notification"
|
|
"github.com/casdoor/casdoor/util"
|
|
notify "github.com/casdoor/notify2"
|
|
)
|
|
|
|
func getNotificationClient(provider *Provider) (notify.Notifier, error) {
|
|
var client notify.Notifier
|
|
client, err := notification.GetNotificationProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.ClientId2, provider.ClientSecret2, provider.AppId, provider.Receiver, provider.Method, provider.Title, provider.Metadata, provider.RegionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func SendNotification(provider *Provider, content string) error {
|
|
client, err := getNotificationClient(provider)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = client.Send(context.Background(), "", content)
|
|
return err
|
|
}
|
|
|
|
// SsoLogoutNotification represents the structure of a session-level SSO logout notification
|
|
// This includes session information and a signature for authentication
|
|
type SsoLogoutNotification struct {
|
|
// User information
|
|
Owner string `json:"owner"`
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"displayName"`
|
|
Email string `json:"email"`
|
|
Phone string `json:"phone"`
|
|
Id string `json:"id"`
|
|
|
|
// Event type
|
|
Event string `json:"event"`
|
|
|
|
// Session-level information for targeted logout
|
|
SessionIds []string `json:"sessionIds"` // List of session IDs being logged out
|
|
AccessTokenHashes []string `json:"accessTokenHashes"` // Hashes of access tokens being expired
|
|
|
|
// Authentication fields to prevent malicious logout requests
|
|
Nonce string `json:"nonce"` // Random nonce for replay protection
|
|
Timestamp int64 `json:"timestamp"` // Unix timestamp of the notification
|
|
Signature string `json:"signature"` // HMAC-SHA256 signature for verification
|
|
}
|
|
|
|
// GetTokensByUser retrieves all tokens for a specific user
|
|
func GetTokensByUser(owner, username string) ([]*Token, error) {
|
|
tokens := []*Token{}
|
|
err := ormer.Engine.Where("organization = ? and user = ?", owner, username).Find(&tokens)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tokens, nil
|
|
}
|
|
|
|
// generateLogoutSignature generates an HMAC-SHA256 signature for the logout notification
|
|
// The signature is computed over the critical fields to prevent tampering
|
|
func generateLogoutSignature(clientSecret string, owner string, name string, nonce string, timestamp int64, sessionIds []string, accessTokenHashes []string) string {
|
|
// Create a deterministic string from all fields that need to be verified
|
|
// Use strings.Join to avoid trailing separators and improve performance
|
|
sessionIdsStr := strings.Join(sessionIds, ",")
|
|
tokenHashesStr := strings.Join(accessTokenHashes, ",")
|
|
|
|
data := fmt.Sprintf("%s|%s|%s|%d|%s|%s", owner, name, nonce, timestamp, sessionIdsStr, tokenHashesStr)
|
|
return util.GetHmacSha256(clientSecret, data)
|
|
}
|
|
|
|
// SendSsoLogoutNotifications sends logout notifications to all notification providers
|
|
// configured in the user's signup application
|
|
func SendSsoLogoutNotifications(user *User, sessionIds []string, tokens []*Token) error {
|
|
if user == nil {
|
|
return nil
|
|
}
|
|
|
|
// If user's signup application is empty, don't send notifications
|
|
if user.SignupApplication == "" {
|
|
return nil
|
|
}
|
|
|
|
// Get the user's signup application
|
|
application, err := GetApplicationByUser(user)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get signup application: %w", err)
|
|
}
|
|
|
|
if application == nil {
|
|
return fmt.Errorf("signup application not found: %s", user.SignupApplication)
|
|
}
|
|
|
|
// Extract access token hashes from tokens
|
|
accessTokenHashes := make([]string, 0, len(tokens))
|
|
for _, token := range tokens {
|
|
if token.AccessTokenHash != "" {
|
|
accessTokenHashes = append(accessTokenHashes, token.AccessTokenHash)
|
|
}
|
|
}
|
|
|
|
// Generate nonce and timestamp for replay protection
|
|
nonce := util.GenerateId()
|
|
timestamp := time.Now().Unix()
|
|
|
|
// Generate signature using the application's client secret
|
|
signature := generateLogoutSignature(
|
|
application.ClientSecret,
|
|
user.Owner,
|
|
user.Name,
|
|
nonce,
|
|
timestamp,
|
|
sessionIds,
|
|
accessTokenHashes,
|
|
)
|
|
|
|
// Prepare the notification data
|
|
notificationObj := SsoLogoutNotification{
|
|
Owner: user.Owner,
|
|
Name: user.Name,
|
|
DisplayName: user.DisplayName,
|
|
Email: user.Email,
|
|
Phone: user.Phone,
|
|
Id: user.Id,
|
|
Event: "sso-logout",
|
|
SessionIds: sessionIds,
|
|
AccessTokenHashes: accessTokenHashes,
|
|
Nonce: nonce,
|
|
Timestamp: timestamp,
|
|
Signature: signature,
|
|
}
|
|
|
|
notificationData, err := json.Marshal(notificationObj)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal user data: %w", err)
|
|
}
|
|
content := string(notificationData)
|
|
|
|
// Send notifications to all notification providers in the signup application
|
|
for _, providerItem := range application.Providers {
|
|
if providerItem.Provider == nil {
|
|
continue
|
|
}
|
|
|
|
// Only send to notification providers
|
|
if providerItem.Provider.Category != "Notification" {
|
|
continue
|
|
}
|
|
|
|
// Send the notification using the provider from the providerItem
|
|
err = SendNotification(providerItem.Provider, content)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send SSO logout notification to provider %s/%s: %w", providerItem.Provider.Owner, providerItem.Provider.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifySsoLogoutSignature verifies the signature of an SSO logout notification
|
|
// This should be called by applications receiving logout notifications
|
|
func VerifySsoLogoutSignature(clientSecret string, notification *SsoLogoutNotification) bool {
|
|
expectedSignature := generateLogoutSignature(
|
|
clientSecret,
|
|
notification.Owner,
|
|
notification.Name,
|
|
notification.Nonce,
|
|
notification.Timestamp,
|
|
notification.SessionIds,
|
|
notification.AccessTokenHashes,
|
|
)
|
|
return notification.Signature == expectedSignature
|
|
}
|