@@ -71,7 +71,15 @@ public DevAuthHandler(
7171
7272 protected override Task < AuthenticateResult > HandleAuthenticateAsync ( )
7373 {
74- var roles = ReadRoles ( ) ;
74+ // PRIORITY 1: If the request carries a real JWT (e.g. from /api/auth/login),
75+ // authenticate as the real user and skip dev-mode entirely.
76+ var realJwtResult = TryAuthenticateRealJwt ( ) ;
77+ if ( realJwtResult is not null )
78+ return Task . FromResult ( realJwtResult ) ;
79+
80+ // PRIORITY 2: Dev-mode auth — cookie or dev-prefixed bearer header.
81+ // Only reached when no valid real JWT is present.
82+ var roles = ReadDevRoles ( ) ;
7583 if ( roles is null || roles . Count == 0 )
7684 {
7785 return Task . FromResult ( AuthenticateResult . NoResult ( ) ) ;
@@ -101,38 +109,29 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
101109 return Task . FromResult ( AuthenticateResult . Success ( ticket ) ) ;
102110 }
103111
104- private List < string > ? ReadRoles ( )
112+ /// <summary>
113+ /// Attempts to validate the Authorization header as a real JWT issued by
114+ /// <c>/api/auth/login</c>. Returns <c>null</c> when no header is present,
115+ /// the token is invalid, or it is a dev-mode token.
116+ /// </summary>
117+ private AuthenticateResult ? TryAuthenticateRealJwt ( )
105118 {
106- // Prefer cookie (browser path); fall back to bearer header (curl / Postman).
107- if ( Request . Cookies . TryGetValue ( DevCookieName , out var cookieValue ) && ! string . IsNullOrEmpty ( cookieValue ) )
108- {
109- return new List < string > { cookieValue . Trim ( ) } ;
110- }
119+ if ( ! Request . Headers . TryGetValue ( "Authorization" , out var auth ) )
120+ return null ;
111121
112- if ( Request . Headers . TryGetValue ( "Authorization" , out var auth ) )
113- {
114- var raw = auth . ToString ( ) ;
122+ var raw = auth . ToString ( ) ;
115123
116- const string devPrefix = "Bearer dev:" ;
117- if ( raw . StartsWith ( devPrefix , StringComparison . OrdinalIgnoreCase ) )
118- {
119- return new List < string > { raw . Substring ( devPrefix . Length ) . Trim ( ) } ;
120- }
124+ // Skip dev-prefixed tokens — they are handled by the dev-mode path.
125+ const string devPrefix = "Bearer dev:" ;
126+ if ( raw . StartsWith ( devPrefix , StringComparison . OrdinalIgnoreCase ) )
127+ return null ;
121128
122- // Fallback: try to decode as a real JWT (e.g. issued by /api/auth/login)
123- const string bearerPrefix = "Bearer " ;
124- if ( raw . StartsWith ( bearerPrefix , StringComparison . OrdinalIgnoreCase ) )
125- {
126- var token = raw . Substring ( bearerPrefix . Length ) . Trim ( ) ;
127- return TryReadRolesFromJwt ( token ) ;
128- }
129- }
129+ const string bearerPrefix = "Bearer " ;
130+ if ( ! raw . StartsWith ( bearerPrefix , StringComparison . OrdinalIgnoreCase ) )
131+ return null ;
130132
131- return null ;
132- }
133+ var token = raw . Substring ( bearerPrefix . Length ) . Trim ( ) ;
133134
134- private List < string > ? TryReadRolesFromJwt ( string token )
135- {
136135 var opts = _localAuthOptions . Value ;
137136 var profiles = new [ ] { opts . External , opts . Internal } ;
138137 var handler = new JwtSecurityTokenHandler { MapInboundClaims = false } ;
@@ -155,7 +154,7 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
155154 ClockSkew = TimeSpan . FromMinutes ( 2 ) ,
156155 } ;
157156
158- ClaimsPrincipal ? principal ;
157+ ClaimsPrincipal principal ;
159158 try
160159 {
161160 principal = handler . ValidateToken ( token , parameters , out _ ) ;
@@ -166,12 +165,61 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
166165 continue ;
167166 }
168167
169- var roles = principal . FindAll ( "roles" ) . Select ( c => c . Value ) . ToList ( ) ;
170- if ( roles . Count > 0 )
171- return roles ;
168+ // Extract claims directly from the validated JWT — do NOT remap to dev users.
169+ var sub = principal . FindFirstValue ( "sub" )
170+ ?? principal . FindFirstValue ( ClaimTypes . NameIdentifier ) ;
171+ if ( string . IsNullOrEmpty ( sub ) )
172+ continue ;
173+
174+ var email = principal . FindFirstValue ( "email" ) ?? string . Empty ;
175+ var preferredUsername = principal . FindFirstValue ( "preferred_username" ) ?? email ;
176+ var name = principal . FindFirstValue ( "name" )
177+ ?? principal . FindFirstValue ( ClaimTypes . Name )
178+ ?? preferredUsername ;
179+
180+ var claims = new List < Claim >
181+ {
182+ new ( "sub" , sub ) ,
183+ new ( "oid" , sub ) ,
184+ new ( "preferred_username" , preferredUsername ) ,
185+ new ( "name" , name ) ,
186+ new ( "email" , email ) ,
187+ } ;
188+ claims . AddRange ( principal . FindAll ( "roles" ) . Select ( c => new Claim ( "roles" , c . Value ) ) ) ;
189+
190+ var identity = new ClaimsIdentity ( claims , SchemeName , "preferred_username" , "roles" ) ;
191+ var realPrincipal = new ClaimsPrincipal ( identity ) ;
192+ var ticket = new AuthenticationTicket ( realPrincipal , SchemeName ) ;
193+ return AuthenticateResult . Success ( ticket ) ;
194+ }
195+
196+ Logger . LogDebug ( "No valid real JWT found in DevAuthHandler; falling back to dev-mode auth" ) ;
197+ return null ;
198+ }
199+
200+ /// <summary>
201+ /// Reads dev-mode credentials from cookie or the <c>Bearer dev:<role></c> header.
202+ /// Returns <c>null</c> when neither is present.
203+ /// </summary>
204+ private List < string > ? ReadDevRoles ( )
205+ {
206+ // Prefer bearer header (curl / Postman) over cookie.
207+ if ( Request . Headers . TryGetValue ( "Authorization" , out var auth ) )
208+ {
209+ var raw = auth . ToString ( ) ;
210+ const string devPrefix = "Bearer dev:" ;
211+ if ( raw . StartsWith ( devPrefix , StringComparison . OrdinalIgnoreCase ) )
212+ {
213+ return new List < string > { raw . Substring ( devPrefix . Length ) . Trim ( ) } ;
214+ }
215+ }
216+
217+ // Fall back to cookie (browser path).
218+ if ( Request . Cookies . TryGetValue ( DevCookieName , out var cookieValue ) && ! string . IsNullOrEmpty ( cookieValue ) )
219+ {
220+ return new List < string > { cookieValue . Trim ( ) } ;
172221 }
173222
174- Logger . LogWarning ( "JWT validation failed for all profiles in DevAuthHandler" ) ;
175223 return null ;
176224 }
177225}
0 commit comments