支持 Manifest V2 动态 tid 验证、Tenant Context 同步和租户内 JIT 投影,并保留 Manifest V1 与旧 Session 兼容。\n\n增加 tenantHint、租户切换、普通注册关闭及 application/principal/tenant 两级 SSF 撤销;迁移、定向安全测试和本地双租户跨仓 E2E 已通过。\n\nrelease_required=true;未执行 Release、Staging 或真实链路。
94 lines
3.0 KiB
Go
94 lines
3.0 KiB
Go
package oidcsession
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const LoginTransactionCookieName = "easyai_gateway_oidc_login"
|
|
|
|
var loginTransactionAAD = []byte("easyai-gateway/oidc-login-transaction/v1")
|
|
|
|
type LoginTransaction struct {
|
|
State string `json:"state"`
|
|
Nonce string `json:"nonce"`
|
|
PKCEVerifier string `json:"pkceVerifier"`
|
|
ReturnTo string `json:"returnTo"`
|
|
TenantHint string `json:"tenantHint,omitempty"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
}
|
|
|
|
func NewLoginTransaction(returnTo string, now time.Time) (LoginTransaction, error) {
|
|
return NewLoginTransactionWithTenantHint(returnTo, "", now)
|
|
}
|
|
|
|
func NewLoginTransactionWithTenantHint(returnTo, tenantHint string, now time.Time) (LoginTransaction, error) {
|
|
if !ValidReturnTo(returnTo) {
|
|
return LoginTransaction{}, errors.New("returnTo must be a same-origin relative path")
|
|
}
|
|
state, err := randomBase64URL(32)
|
|
if err != nil {
|
|
return LoginTransaction{}, err
|
|
}
|
|
nonce, err := randomBase64URL(32)
|
|
if err != nil {
|
|
return LoginTransaction{}, err
|
|
}
|
|
verifier, err := randomBase64URL(32)
|
|
if err != nil {
|
|
return LoginTransaction{}, err
|
|
}
|
|
return LoginTransaction{
|
|
State: state, Nonce: nonce, PKCEVerifier: verifier,
|
|
ReturnTo: returnTo, TenantHint: strings.TrimSpace(tenantHint), CreatedAt: now.UTC(),
|
|
}, nil
|
|
}
|
|
|
|
func (c *Cipher) EncodeLoginTransaction(transaction LoginTransaction) (string, error) {
|
|
payload, err := c.SealJSON(transaction, loginTransactionAAD)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.RawURLEncoding.EncodeToString(payload), nil
|
|
}
|
|
|
|
func (c *Cipher) DecodeLoginTransaction(encoded string, now time.Time) (LoginTransaction, error) {
|
|
payload, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(encoded))
|
|
if err != nil {
|
|
return LoginTransaction{}, errors.New("OIDC login transaction is invalid")
|
|
}
|
|
var transaction LoginTransaction
|
|
if err := c.OpenJSON(payload, loginTransactionAAD, &transaction); err != nil {
|
|
return LoginTransaction{}, err
|
|
}
|
|
if transaction.State == "" || transaction.Nonce == "" || transaction.PKCEVerifier == "" || !ValidReturnTo(transaction.ReturnTo) ||
|
|
transaction.TenantHint != "" && uuid.Validate(transaction.TenantHint) != nil ||
|
|
transaction.CreatedAt.IsZero() || now.Before(transaction.CreatedAt.Add(-time.Minute)) || !now.Before(transaction.CreatedAt.Add(10*time.Minute)) {
|
|
return LoginTransaction{}, errors.New("OIDC login transaction has expired or is invalid")
|
|
}
|
|
return transaction, nil
|
|
}
|
|
|
|
func ValidReturnTo(value string) bool {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || !strings.HasPrefix(value, "/") || strings.HasPrefix(value, "//") || strings.Contains(value, "\\") {
|
|
return false
|
|
}
|
|
parsed, err := url.Parse(value)
|
|
return err == nil && !parsed.IsAbs() && parsed.Host == ""
|
|
}
|
|
|
|
func randomBase64URL(size int) (string, error) {
|
|
value := make([]byte, size)
|
|
if _, err := rand.Read(value); err != nil {
|
|
return "", err
|
|
}
|
|
return base64.RawURLEncoding.EncodeToString(value), nil
|
|
}
|