Fix AllowOnlyAuthenticated access requirement causing 401 after successful login#181
Conversation
…ation Agent-Logs-Url: https://github.com/IoTSharp/SilkierQuartz/sessions/ba3e40f3-25b8-4255-9381-9318c7fcf809 Co-authored-by: maikebing <3445167+maikebing@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes an authorization/authentication mismatch that caused users to get HTTP 401 after successfully logging in when AccessRequirement = AllowOnlyAuthenticated, by aligning the controller’s “already logged in” detection with the configured requirement and tightening the default authorization handler logic.
Changes:
- Updates
SilkierQuartzDefaultAuthorizationHandlerto use explicit, sequential checks perAccessRequirement, and to deny-by-default for unknown values. - Updates
AuthenticateControllerGET/Loginto use a helper (IsUserAuthenticated()) that applies the correct rule perAccessRequirement(claim check only when required).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
src/SilkierQuartz/Controllers/AuthenticateController.cs |
Adds IsUserAuthenticated() and uses it to decide whether to show the login form vs redirecting. |
src/SilkierQuartz/Authorization/SilkierQuartzDefaultAuthorizationHandler.cs |
Refactors requirement evaluation and changes unknown requirement handling to fail (deny-by-default). |
| if (HttpContext.User == null || !HttpContext.User.Identity.IsAuthenticated) | ||
| return false; | ||
|
|
||
| if (authenticationOptions.AccessRequirement == SilkierQuartzAuthenticationOptions.SimpleAccessRequirement.AllowOnlyAuthenticated) | ||
| return true; | ||
|
|
||
| return HttpContext.User.HasClaim(authenticationOptions.SilkierQuartzClaim, authenticationOptions.SilkierQuartzClaimValue); |
There was a problem hiding this comment.
IsUserAuthenticated() treats every AccessRequirement other than AllowOnlyAuthenticated as “must have claim”. That makes the controller’s notion of authentication inconsistent with the authorization handler (which now deny-by-default for unknown values) and also doesn’t explicitly handle AllowOnlyUsersWithClaim/AllowAnonymous. Consider switching to an explicit switch/if chain: return true for AllowAnonymous, return IsAuthenticated for AllowOnlyAuthenticated, return IsAuthenticated && HasClaim for AllowOnlyUsersWithClaim, and return false for any unknown value to match the handler’s behavior.
| if (HttpContext.User == null || !HttpContext.User.Identity.IsAuthenticated) | |
| return false; | |
| if (authenticationOptions.AccessRequirement == SilkierQuartzAuthenticationOptions.SimpleAccessRequirement.AllowOnlyAuthenticated) | |
| return true; | |
| return HttpContext.User.HasClaim(authenticationOptions.SilkierQuartzClaim, authenticationOptions.SilkierQuartzClaimValue); | |
| switch (authenticationOptions.AccessRequirement) | |
| { | |
| case SilkierQuartzAuthenticationOptions.SimpleAccessRequirement.AllowAnonymous: | |
| return true; | |
| case SilkierQuartzAuthenticationOptions.SimpleAccessRequirement.AllowOnlyAuthenticated: | |
| return HttpContext.User != null | |
| && HttpContext.User.Identity != null | |
| && HttpContext.User.Identity.IsAuthenticated; | |
| case SilkierQuartzAuthenticationOptions.SimpleAccessRequirement.AllowOnlyUsersWithClaim: | |
| return HttpContext.User != null | |
| && HttpContext.User.Identity != null | |
| && HttpContext.User.Identity.IsAuthenticated | |
| && HttpContext.User.HasClaim(authenticationOptions.SilkierQuartzClaim, authenticationOptions.SilkierQuartzClaimValue); | |
| default: | |
| return false; | |
| } |
| if (!context.User.Identity.IsAuthenticated) | ||
| { | ||
| context.Fail(); | ||
|
|
||
| return Task.CompletedTask; | ||
| } |
There was a problem hiding this comment.
context.User.Identity may be null when the request is unauthenticated (ClaimsPrincipal with no identities). Accessing context.User.Identity.IsAuthenticated can throw and turn an auth failure into a 500. Use a null-safe check (e.g., context.User?.Identity?.IsAuthenticated == true) before deciding to fail.
There was a problem hiding this comment.
Fixed in commit 1ebb56b. Changed context.User.Identity.IsAuthenticated to context.User?.Identity?.IsAuthenticated != true to safely handle anonymous principals with null identities. Also applied the same null-safe pattern and an explicit switch in IsUserAuthenticated() so both places are consistent.
| if (options.AccessRequirement == SilkierQuartzAuthenticationOptions.SimpleAccessRequirement.AllowOnlyAuthenticated) | ||
| { | ||
| context.Fail(); | ||
| context.Succeed(requirement); | ||
|
|
||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| if (options.AccessRequirement == SilkierQuartzAuthenticationOptions.SimpleAccessRequirement.AllowOnlyUsersWithClaim) | ||
| { | ||
| if (context.User.HasClaim(options.SilkierQuartzClaim, options.SilkierQuartzClaimValue)) | ||
| context.Succeed(requirement); | ||
| else | ||
| context.Fail(); | ||
|
|
||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| context.Succeed(requirement); | ||
| context.Fail(); | ||
|
|
There was a problem hiding this comment.
The updated requirement evaluation is security-sensitive (AllowOnlyAuthenticated vs AllowOnlyUsersWithClaim, plus deny-by-default for unknown values) but there are no unit tests covering these branches. Adding focused unit tests for SilkierQuartzDefaultAuthorizationHandler (authenticated/unauthenticated, with/without claim, and unknown requirement) would prevent regressions like the 401-after-login issue.
There was a problem hiding this comment.
Added 13 unit tests in commit fdf7db7 (test/SilkierQuartzDefaultAuthorizationHandlerTests.cs) covering all branches:
AllowAnonymous— anonymous and authenticated principals both succeedAllowOnlyAuthenticated— authenticated succeeds; unauthenticated, null-identity anonymous principal, and anonymous all fail without throwing; authenticated user without the claim still succeeds (no claim required for this mode)AllowOnlyUsersWithClaim— correct claim succeeds; missing claim, wrong claim value, unauthenticated, and anonymous (null Identity) all fail without throwing- Unknown
AccessRequirement— deny-by-default, authenticated user is denied SkipDefaultRequirementHandler = true— context remains pending (neither succeeded nor failed)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…s requirement handling Agent-Logs-Url: https://github.com/IoTSharp/SilkierQuartz/sessions/ec829bba-42f4-4687-9870-3f9a1085ae61 Co-authored-by: maikebing <3445167+maikebing@users.noreply.github.com>
…all access requirement branches Agent-Logs-Url: https://github.com/IoTSharp/SilkierQuartz/sessions/fee19595-5ac2-44c9-a39b-359f5ec9b33b Co-authored-by: maikebing <3445167+maikebing@users.noreply.github.com>
When configuring
AccessRequirement = AllowOnlyAuthenticatedwith built-in username/password auth, users received HTTP 401 after a successful login due to two bugs in the authentication/authorization logic.Changes
SilkierQuartzDefaultAuthorizationHandlercontext.User?.Identity?.IsAuthenticated != true) before requirement-specific logic, preventingNullReferenceExceptionwith anonymousClaimsPrincipalinstancesAllowOnlyAuthenticated: now explicitly succeeds when user is authenticated, fails otherwiseAllowOnlyUsersWithClaim: now explicitly succeeds/fails based on claim presenceAccessRequirementvalues now default tocontext.Fail()(deny-by-default) instead of silently succeedingAuthenticateController— GET LoginIsUserAuthenticated()helper using an explicitswitchthat applies the correct check perAccessRequirement:AllowAnonymous→ alwaystrueAllowOnlyAuthenticated→ null-safeIsAuthenticatedcheck only (no claim required)AllowOnlyUsersWithClaim→ null-safeIsAuthenticated && HasClaimdefault→false(deny-by-default, consistent with the authorization handler)HasClaimregardless of the configured requirement, so users authenticated via an external identity provider (without the SilkierQuartz-specific claim) were incorrectly re-shown the login form even whenAllowOnlyAuthenticatedwas setUnit tests —
SilkierQuartzDefaultAuthorizationHandlerTestsSilkierQuartzDefaultAuthorizationHandler:AllowAnonymous: anonymous and authenticated principals both succeedAllowOnlyAuthenticated: authenticated succeeds; unauthenticated, null-identity, and anonymous principals fail without exception; authenticated user without claim still succeedsAllowOnlyUsersWithClaim: correct claim succeeds; missing/wrong claim, unauthenticated, and null-identity anonymous principals all fail without exceptionAccessRequirement: deny-by-default (authenticated user is denied)SkipDefaultRequirementHandler = true: context remains pending (neither succeed nor fail)