@@ -18,6 +18,7 @@ import (
1818 "sort"
1919 "strings"
2020 "sync"
21+ "sync/atomic"
2122 "time"
2223
2324 "connectrpc.com/connect"
@@ -108,72 +109,72 @@ type Authentication struct {
108109}
109110
110111// dpopNonceManager manages server-issued DPoP nonces per RFC 9449 §8
111- type dpopNonceManager struct {
112- mu sync.RWMutex
112+ type nonceState struct {
113113 currentNonce string
114114 previousNonce string
115- expiration time.Duration
116- lastRotation time.Time
117- requireNonce bool
115+ rotatedAt time.Time
116+ }
117+
118+ type dpopNonceManager struct {
119+ state atomic.Pointer [nonceState ]
120+ mu sync.Mutex // serializes rotation only
121+ expiration time.Duration
122+ requireNonce bool
118123}
119124
120125func newDPoPNonceManager (requireNonce bool , expiration time.Duration ) * dpopNonceManager {
121126 nm := & dpopNonceManager {
122127 requireNonce : requireNonce ,
123128 expiration : expiration ,
124129 }
130+ nm .state .Store (& nonceState {})
125131 if requireNonce {
126132 nm .rotate ()
127133 }
128134 return nm
129135}
130136
131- func (nm * dpopNonceManager ) rotate () {
132- nm .mu .Lock ()
133- defer nm .mu .Unlock ()
134-
135- nm .previousNonce = nm .currentNonce
137+ func (nm * dpopNonceManager ) storeRotated (s * nonceState ) {
136138 nonce := make ([]byte , dpopNonceBytes )
137139 if _ , err := rand .Read (nonce ); err != nil {
138140 panic (fmt .Sprintf ("failed to generate nonce: %v" , err ))
139141 }
140- nm .currentNonce = hex .EncodeToString (nonce )
141- nm .lastRotation = time .Now ()
142+ nm .state .Store (& nonceState {
143+ previousNonce : s .currentNonce ,
144+ currentNonce : hex .EncodeToString (nonce ),
145+ rotatedAt : time .Now (),
146+ })
147+ }
148+
149+ func (nm * dpopNonceManager ) rotate () {
150+ nm .mu .Lock ()
151+ defer nm .mu .Unlock ()
152+ nm .storeRotated (nm .state .Load ())
142153}
143154
144155func (nm * dpopNonceManager ) getCurrentNonce () string {
145- nm .mu .RLock ()
146- if time .Since (nm .lastRotation ) <= nm .expiration {
147- defer nm .mu .RUnlock ()
148- return nm .currentNonce
156+ s := nm .state .Load ()
157+ if time .Since (s .rotatedAt ) <= nm .expiration {
158+ return s .currentNonce
149159 }
150- nm .mu .RUnlock ()
151160
152161 nm .mu .Lock ()
153162 defer nm .mu .Unlock ()
154- // Double-check after acquiring the write lock to prevent concurrent double-rotation.
155- if time .Since (nm .lastRotation ) > nm .expiration {
156- nm .previousNonce = nm .currentNonce
157- nonce := make ([]byte , dpopNonceBytes )
158- if _ , err := rand .Read (nonce ); err != nil {
159- panic (fmt .Sprintf ("failed to generate nonce: %v" , err ))
160- }
161- nm .currentNonce = hex .EncodeToString (nonce )
162- nm .lastRotation = time .Now ()
163+ // Double-check after acquiring the lock to prevent concurrent double-rotation.
164+ s = nm .state .Load ()
165+ if time .Since (s .rotatedAt ) > nm .expiration {
166+ nm .storeRotated (s )
167+ s = nm .state .Load ()
163168 }
164-
165- return nm .currentNonce
169+ return s .currentNonce
166170}
167171
168172func (nm * dpopNonceManager ) validateNonce (nonce string ) bool {
169173 if ! nm .requireNonce {
170174 return true
171175 }
172-
173- nm .mu .RLock ()
174- defer nm .mu .RUnlock ()
175-
176- return nonce == nm .currentNonce || nonce == nm .previousNonce
176+ s := nm .state .Load ()
177+ return nonce != "" && (nonce == s .currentNonce || nonce == s .previousNonce )
177178}
178179
179180// Creates new authN which is used to verify tokens for a set of given issuers
0 commit comments