fix: harden identity, live access, drafts and setup (review only)
Source-reviewed hardening changes and regression tests. UNBUILT / UNTESTED: not a deployable release. Go formatting, compilation, automated regression tests, full frontend validation and staging acceptance remain required. See docs/hardening/VALIDATION.md. Main remains the original clean snapshot; no upstream Git history imported.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
// Modified in the independent hardening fork; see docs/hardening.
|
||||
|
||||
package openidentity
|
||||
|
||||
import (
|
||||
@@ -269,3 +271,25 @@ func flipLastChar(signature string) string {
|
||||
func itoa(value int64) string {
|
||||
return strconv.FormatInt(value, 10)
|
||||
}
|
||||
|
||||
func TestRequireSignedIdentityRejectsUnsignedAndInvalidSignatures(t *testing.T) {
|
||||
for _, signature := range []string{"", "invalid"} {
|
||||
ctx, _ := newIdentityContext(map[string]string{"X-External-Id": testUserID, "X-External-Id-Signature": signature})
|
||||
if _, err := GetExternalUser(ctx, Secrets{ChannelID: testChannelID, IdentityHMAC: testSecret, RequireSignedIdentity: true}); err == nil { t.Fatal("signed-only policy accepted an unsigned or invalid identity") }
|
||||
}
|
||||
ctx, _ := newIdentityContext(signedIdentityHeaders(testSecret,testChannelID,testUserID,time.Now()))
|
||||
user, err := GetExternalUser(ctx, Secrets{ChannelID:testChannelID,IdentityHMAC:testSecret,RequireSignedIdentity:true})
|
||||
if err != nil || user.IdentityScope != IdentityScope(testChannelID,"hmac") { t.Fatal("valid signed identity did not retain its HMAC scope") }
|
||||
}
|
||||
|
||||
func TestIdentityScopeSeparatesIssuersAndChannels(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
for _, channel := range []string{"channel-one","channel-two"} {
|
||||
for _, issuer := range []string{"guest","hmac","jwt"} {
|
||||
scope := IdentityScope(channel,issuer)
|
||||
if scope == "" || seen[scope] { t.Fatal("identity scopes collided") }
|
||||
seen[scope] = true
|
||||
}
|
||||
}
|
||||
if IdentityScope("", "hmac") != "" || IdentityScope("channel", "unknown") != "" { t.Fatal("invalid scope inputs accepted") }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Modified in the independent hardening fork; see docs/hardening.
|
||||
|
||||
package openidentity
|
||||
|
||||
import (
|
||||
@@ -24,6 +26,9 @@ type ExternalUser struct {
|
||||
ExternalSource enums.ExternalSource `json:"externalSource"`
|
||||
ExternalID string `json:"externalId"`
|
||||
ExternalName string `json:"externalName"`
|
||||
// IdentityScope and ChannelID are server-established authorization context, never request fields.
|
||||
IdentityScope string `json:"-"`
|
||||
ChannelID int64 `json:"-"`
|
||||
}
|
||||
|
||||
type UserTokenClaims struct {
|
||||
@@ -47,6 +52,7 @@ type Secrets struct {
|
||||
// ChannelID is bound into the signed payload so a signature obtained for one
|
||||
// channel cannot be replayed on another.
|
||||
ChannelID string
|
||||
RequireSignedIdentity bool
|
||||
}
|
||||
|
||||
// DefaultIdentityMaxAge is how long a signed visitor identity stays acceptable
|
||||
@@ -65,9 +71,8 @@ const identityFutureSkewTolerance = 5 * time.Minute
|
||||
//
|
||||
// issuedAt is unix seconds. Lower-case hex output. Three details are deliberate:
|
||||
//
|
||||
// - The channel id is included because CustomerIdentity is keyed by
|
||||
// (source, externalId) globally, so without it a signature minted for one
|
||||
// channel would assert the same identity on every other channel.
|
||||
// - The channel id is included so signatures cannot be replayed on a
|
||||
// different channel, even when a deployment shares an HMAC signing key.
|
||||
// - The timestamp is inside the signed bytes even though it also travels as its
|
||||
// own field, so it cannot be re-dated without breaking the signature. This
|
||||
// bounds how long a leaked pair stays usable; it is not a defence against key
|
||||
@@ -126,6 +131,7 @@ func GetExternalUser(ctx *gin.Context, secrets Secrets) (*ExternalUser, error) {
|
||||
ExternalSource: enums.ExternalSourceUser,
|
||||
ExternalID: claims.UserID,
|
||||
ExternalName: claims.Name,
|
||||
IdentityScope: IdentityScope(secrets.ChannelID, "jwt"),
|
||||
}, nil
|
||||
}
|
||||
return getExternalIDUser(ctx, secrets)
|
||||
@@ -201,13 +207,18 @@ func getExternalIDUser(ctx *gin.Context, secrets Secrets) (*ExternalUser, error)
|
||||
ExternalSource: enums.ExternalSourceUser,
|
||||
ExternalID: externalID,
|
||||
ExternalName: externalName,
|
||||
IdentityScope: IdentityScope(secrets.ChannelID, "hmac"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if secrets.RequireSignedIdentity {
|
||||
return nil, errorsx.UnauthorizedI18n("error.e0161")
|
||||
}
|
||||
return &ExternalUser{
|
||||
ExternalSource: enums.ExternalSourceGuest,
|
||||
ExternalID: externalID,
|
||||
ExternalName: externalName,
|
||||
IdentityScope: IdentityScope(secrets.ChannelID, "guest"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -267,3 +278,24 @@ func decodeExternalDisplayName(s string) string {
|
||||
}
|
||||
return strings.TrimSpace(dec)
|
||||
}
|
||||
|
||||
// IdentityScope separates independent identity issuers, trust levels and channels.
|
||||
// It deliberately does not depend on a secret so ordinary key rotation preserves
|
||||
// the issuer's history. Reassigning a channel to a different issuer requires a new
|
||||
// channel, not silently reusing that issuer's namespace.
|
||||
func IdentityScope(channelCode, issuer string) string {
|
||||
if strings.TrimSpace(channelCode) == "" { return "" }
|
||||
switch issuer { case "jwt", "hmac", "guest": default: return "" }
|
||||
digest := sha256.Sum256([]byte(strings.TrimSpace(channelCode)))
|
||||
return "v1:" + issuer + ":" + hex.EncodeToString(digest[:])
|
||||
}
|
||||
|
||||
func ValidIdentityScope(channelCode string, external ExternalUser) bool {
|
||||
if external.ExternalSource == enums.ExternalSourceGuest {
|
||||
return external.IdentityScope != "" && external.IdentityScope == IdentityScope(channelCode, "guest")
|
||||
}
|
||||
if external.ExternalSource == enums.ExternalSourceUser {
|
||||
return external.IdentityScope != "" && (external.IdentityScope == IdentityScope(channelCode, "jwt") || external.IdentityScope == IdentityScope(channelCode, "hmac"))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user