Skip to content

Commit 1a62435

Browse files
Marko Petzoldclaude
andcommitted
router/realm: expose target realm URI to authenticators
Adds the realm URI to the HELLO Details dict (under the "nexus.session.realm" key) before invoking the realm's authenticator. The HELLO carries the realm as a separate message field from Details, so without this dynamic authenticators that share one instance across multiple realms cannot tell which realm a client is requesting and have no way to route their lookup per-realm. Key is namespaced under "nexus." to avoid collision with future WAMP-spec details fields, mirroring how nexus already namespaces "transport.auth.*" entries. Pinned by TestRealmExposesURIToAuthenticator (a stub Authenticator that records the details it receives and asserts the key is present with the right value). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0aefb4d commit 1a62435

2 files changed

Lines changed: 104 additions & 0 deletions

File tree

router/realm.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ type testamentBucket struct {
3737
// authentication and authorization. WAMP messages are only routed within a
3838
// Realm.
3939
type realm struct {
40+
uri wamp.URI
41+
4042
broker *broker
4143
dealer *dealer
4244

@@ -99,6 +101,7 @@ func newRealm(config *RealmConfig, broker *broker, dealer *dealer, logger stdlog
99101
}
100102

101103
r := &realm{
104+
uri: config.URI,
102105
broker: broker,
103106
dealer: dealer,
104107
authorizer: config.Authorizer,
@@ -740,6 +743,14 @@ func (r *realm) authClient(sid wamp.ID, client wamp.Peer, details wamp.Dict) (*w
740743
return nil, errors.New("could not authenticate with any method")
741744
}
742745

746+
// Expose the target realm URI to the authenticator. The HELLO carries
747+
// the realm as a separate message field from Details, so without this
748+
// dynamic authenticators that share one instance across multiple realms
749+
// can't tell which realm the client is requesting. Key is namespaced
750+
// under "nexus." to avoid collision with future WAMP-spec details
751+
// fields (mirrors how nexus already namespaces "transport.auth.*").
752+
details["nexus.session.realm"] = string(r.uri)
753+
743754
// Return welcome message or error.
744755
welcome, err := authr.Authenticate(sid, details, client)
745756
if err != nil {

router/realm_authenticator_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package router //nolint:testpackage
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/gammazero/nexus/v3/router/auth"
10+
"github.com/gammazero/nexus/v3/transport"
11+
"github.com/gammazero/nexus/v3/wamp"
12+
)
13+
14+
// captureRealmAuthenticator is a stub authenticator that records the
15+
// details dict it receives, then returns a WELCOME. Used to verify that
16+
// the realm injects "nexus.session.realm" into the details before
17+
// calling Authenticate.
18+
type captureRealmAuthenticator struct {
19+
gotDetails wamp.Dict
20+
}
21+
22+
func (a *captureRealmAuthenticator) Authenticate(sid wamp.ID, details wamp.Dict, _ wamp.Peer) (*wamp.Welcome, error) {
23+
// Copy the dict so the caller can't mutate our recorded view via the
24+
// returned welcome.
25+
a.gotDetails = wamp.Dict{}
26+
for k, v := range details {
27+
a.gotDetails[k] = v
28+
}
29+
return &wamp.Welcome{
30+
ID: sid,
31+
Details: wamp.Dict{"authrole": "tester"},
32+
}, nil
33+
}
34+
35+
func (a *captureRealmAuthenticator) AuthMethod() string { return "capture-realm" }
36+
37+
// TestRealmExposesURIToAuthenticator pins that realm.authClient injects
38+
// the target realm URI into the HELLO Details under the
39+
// "nexus.session.realm" key before invoking the authenticator. Dynamic
40+
// authenticators that share one instance across multiple realms (e.g.
41+
// a single auth-server delegate) rely on this to route per-realm.
42+
func TestRealmExposesURIToAuthenticator(t *testing.T) {
43+
const realmURI = wamp.URI("nexus.test.realm.with.auth.capture")
44+
45+
captor := &captureRealmAuthenticator{}
46+
47+
r, err := NewRouter(&Config{
48+
RealmConfigs: []*RealmConfig{
49+
{
50+
URI: realmURI,
51+
StrictURI: false,
52+
Authenticators: []auth.Authenticator{captor},
53+
RequireLocalAuth: true, // LinkedPeers are local — opt into auth
54+
},
55+
},
56+
}, logger)
57+
require.NoError(t, err)
58+
// Router shutdown also disposes attached peers; do NOT call peer.Close
59+
// manually here — main's localPeer panics on double-close.
60+
t.Cleanup(func() { r.Close() })
61+
62+
client, server := transport.LinkedPeers()
63+
64+
go func() { _ = r.Attach(server) }()
65+
66+
hello := &wamp.Hello{
67+
Realm: realmURI,
68+
Details: wamp.Dict{
69+
"authmethods": wamp.List{"capture-realm"},
70+
"roles": wamp.Dict{"caller": wamp.Dict{}},
71+
},
72+
}
73+
client.Send() <- hello
74+
75+
select {
76+
case msg := <-client.Recv():
77+
_, ok := msg.(*wamp.Welcome)
78+
require.True(t, ok, "expected WELCOME, got %T", msg)
79+
case <-time.After(2 * time.Second):
80+
t.Fatal("timeout waiting for WELCOME")
81+
}
82+
83+
require.NotNil(t, captor.gotDetails, "Authenticate was never called")
84+
got, ok := captor.gotDetails["nexus.session.realm"]
85+
require.True(t, ok, "nexus.session.realm missing from authenticator details (got: %v)", captor.gotDetails)
86+
require.Equal(t, string(realmURI), got,
87+
"nexus.session.realm should equal the HELLO target realm")
88+
89+
// Sanity: the underscore-prefixed key from the previous internal
90+
// shape is NOT what we use anymore — guards against drift.
91+
_, oldKey := captor.gotDetails["_realm"]
92+
require.False(t, oldKey, "legacy _realm key should not be set")
93+
}

0 commit comments

Comments
 (0)