Set access token server side#1387
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds middleware that sets an Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Browser
participant Server as HTTP Server
participant Middleware as AuthCookieSetter
participant Resolver as GraphQL Resolver
Client->>Server: POST /graphql (mutation)
Server->>Middleware: pass request (ResponseWriter wrapped)
Middleware->>Resolver: invoke resolver
Resolver->>Resolver: generate token
Resolver->>Resolver: store token in context key
Resolver-->>Middleware: return response
Middleware->>Middleware: on WriteHeader read token from context
Middleware->>Client: add Set-Cookie: auth-token
Middleware-->>Client: send response body
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip You can make CodeRabbit's review stricter and more nitpicky using the `assertive` profile, if that's what you prefer.Change the |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
api/graphql/auth/auth_cookie_setter_test.go (1)
38-80: Add coverage forWriteHeader-first handlers.Current cases won’t catch regressions where headers are committed before
Write. A dedicated test for that path would harden this middleware.🧪 Example additional test case
for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest("GET", "/graphql", nil) @@ assert.Equal(t, tc.responseAuthCookieVal, authTokenValue) }) } + + t.Run("Handler calling WriteHeader before Write still sets auth cookie", func(t *testing.T) { + req := httptest.NewRequest("GET", "/graphql", nil) + handler := auth.AuthCookieSetter(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cookie := auth.ResolverCookieFromContext(r.Context()) + *cookie = "cookie" + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + })) + + recorder := &hijackableRecorder{httptest.NewRecorder()} + handler.ServeHTTP(recorder, req) + + var authTokenValue string + for _, cookie := range recorder.Result().Cookies() { + if cookie.Name == "auth-token" { + authTokenValue = cookie.Value + break + } + } + assert.Equal(t, "cookie", authTokenValue) + }) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/graphql/auth/auth_cookie_setter_test.go` around lines 38 - 80, Add a test case in TestAuthCookieSetterMiddleware that exercises handlers which call WriteHeader (commit headers) before Write to ensure AuthCookieSetter still sets cookies; create a new handler (e.g., writeHeaderFirstHandler) that calls rw.WriteHeader(200) and then writes a body (and optionally sets a cookie value via setResponseAuthCookieHandler logic), plug it into auth.AuthCookieSetter, use the existing hijackableRecorder to capture recorder.Result().Cookies(), and assert the "auth-token" cookie value matches the expected responseAuthCookieVal just like the other cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@api/graphql/auth/auth_cookie_setter.go`:
- Around line 15-25: The Write method currently sets the cookie too late because
headers may already be committed; add an override for
authResponseWriter.WriteHeader that sets the auth-token cookie (using
authTokenFromResolver, SameSiteLaxMode, Path "/", Expires 14 days) before
delegating to w.ResponseWriter.WriteHeader(status), and ensure Write still sets
the cookie if WriteHeader was never called (guard with a boolean like cookieSet
or check header presence) so the Set-Cookie always happens before headers are
sent; reference authResponseWriter.Write, authResponseWriter.WriteHeader,
authTokenFromResolver, and w.ResponseWriter.WriteHeader.
- Around line 17-23: The cookie currently set in auth_cookie_setter.go
(http.SetCookie with Name "auth-token" and Value w.authTokenFromResolver) uses
SameSite: http.SameSiteLaxMode which prevents cross-site fetch/XHR; change the
cookie attributes to SameSite: http.SameSiteNoneMode and add Secure: true (and
keep Path, Expires); update the http.Cookie creation in the same http.SetCookie
call to use SameSiteNoneMode and set Secure to true so the auth cookie is sent
for cross-origin requests when UI and API are on different domains.
---
Nitpick comments:
In `@api/graphql/auth/auth_cookie_setter_test.go`:
- Around line 38-80: Add a test case in TestAuthCookieSetterMiddleware that
exercises handlers which call WriteHeader (commit headers) before Write to
ensure AuthCookieSetter still sets cookies; create a new handler (e.g.,
writeHeaderFirstHandler) that calls rw.WriteHeader(200) and then writes a body
(and optionally sets a cookie value via setResponseAuthCookieHandler logic),
plug it into auth.AuthCookieSetter, use the existing hijackableRecorder to
capture recorder.Result().Cookies(), and assert the "auth-token" cookie value
matches the expected responseAuthCookieVal just like the other cases.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5d7c64c1-e864-4601-995d-403001b32c77
⛔ Files ignored due to path filters (1)
ui/src/components/sidebar/__generated__/sidebarSetExpireShare.tsis excluded by!**/__generated__/**
📒 Files selected for processing (8)
api/graphql/auth/auth.goapi/graphql/auth/auth_cookie_setter.goapi/graphql/auth/auth_cookie_setter_test.goapi/graphql/resolvers/user.goapi/server.goui/src/Pages/LoginPage/InitialSetupPage.tsxui/src/Pages/LoginPage/LoginPage.tsxui/src/Pages/LoginPage/loginUtilities.tsx
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1387 +/- ##
==========================================
+ Coverage 63.37% 63.42% +0.05%
==========================================
Files 203 204 +1
Lines 16673 16715 +42
Branches 604 604
==========================================
+ Hits 10567 10602 +35
- Misses 5695 5701 +6
- Partials 411 412 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…n a different domain
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@api/graphql/auth/auth_cookie_setter.go`:
- Around line 9-13: The authResponseWriter currently embeds http.Hijacker and
performs an unconditional type assertion (w.(http.Hijacker)) which can panic;
change the struct to store a hijacker field (hijacker http.Hijacker) instead of
embedding, update any constructor or wrapper creation to set that field using a
safe type assertion (ok pattern), and add a Hijack() method on
authResponseWriter that checks the hijacker field and returns an error if nil
(rather than panicking) so callers can safely call Hijack(); ensure all
references to the old embedded type use the new Hijack() method or the hijacker
field.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 175bb7cb-5be7-47b4-b065-7711788769ae
📒 Files selected for processing (3)
api/graphql/auth/auth_cookie_setter.goapi/graphql/auth/auth_cookie_setter_test.goapi/server.go
|
This is ready for review |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
api/graphql/auth/auth_cookie_setter_test.go (1)
25-39: Add a middleware success-path test that usesWrite(implicit 200).Current handlers only call
WriteHeader(200). Add one case that writes a body without explicitWriteHeaderto prevent regressions in cookie-setting on implicit status responses.Based on learnings: "When implementing an http.ResponseWriter wrapper for gqlgen (or similar HTTP handlers), ensure you override both WriteHeader and Write so headers/cookies are set consistently in all paths, including the successful response path."💡 Suggested coverage addition
func noResponseAuthCookieHandler() http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) } +func setResponseAuthCookieWriteHandler(token string) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cookie := auth.ResolverCookieFromContext(r.Context()) + *cookie = token + _, _ = w.Write([]byte("ok")) // implicit WriteHeader(200) + }) +} + func TestAuthCookieSetterMiddleware(t *testing.T) { @@ for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ }) } + + t.Run("Login endpoint sets auth cookie on implicit 200 via Write", func(t *testing.T) { + req := httptest.NewRequest("GET", "/graphql", nil) + authHandler := auth.AuthCookieSetter(false)(setResponseAuthCookieWriteHandler("cookie")) + recorder := &hijackableRecorder{httptest.NewRecorder()} + authHandler.ServeHTTP(recorder, req) + + var authToken *http.Cookie + for _, cookie := range recorder.Result().Cookies() { + if cookie.Name == "auth-token" { + authToken = cookie + break + } + } + if authToken == nil { + t.Fatalf("expected auth-token cookie to be set") + } + assert.Equal(t, "cookie", authToken.Value) + }) }Also applies to: 41-44
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/graphql/auth/auth_cookie_setter_test.go` around lines 25 - 39, Add a success-path test that exercises implicit-200 responses by making one handler write a body without calling WriteHeader: update or add a handler (e.g., change noResponseAuthCookieHandler or add a new implicitWriteAuthCookieHandler) to call w.Write([]byte("ok")) and not call WriteHeader, then add a test case that invokes the middleware with that handler and asserts the cookie was set; ensure you reference setResponseAuthCookieHandler and noResponseAuthCookieHandler when locating existing tests to add the new implicit-write case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@api/graphql/auth/auth_cookie_setter_test.go`:
- Around line 90-92: The test currently dereferences authToken.Value without
ensuring authToken is present; add an explicit presence check (e.g.,
assert.NotNil(t, authToken) or if authToken == nil { t.Fatalf(...) }) before
asserting on authToken.Value so a missing cookie fails clearly instead of
panicking; update the block around the existing check that uses
tc.responseAuthCookieVal and tc.uiOnSeparateDomain to first verify authToken is
non-nil, then compare authToken.Value to tc.responseAuthCookieVal and continue
the uiOnSeparateDomain logic.
---
Nitpick comments:
In `@api/graphql/auth/auth_cookie_setter_test.go`:
- Around line 25-39: Add a success-path test that exercises implicit-200
responses by making one handler write a body without calling WriteHeader: update
or add a handler (e.g., change noResponseAuthCookieHandler or add a new
implicitWriteAuthCookieHandler) to call w.Write([]byte("ok")) and not call
WriteHeader, then add a test case that invokes the middleware with that handler
and asserts the cookie was set; ensure you reference
setResponseAuthCookieHandler and noResponseAuthCookieHandler when locating
existing tests to add the new implicit-write case.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c517ce67-115d-4eac-a0a0-d2ac1ecfaf55
📒 Files selected for processing (1)
api/graphql/auth/auth_cookie_setter_test.go
|
@tyler-hagen, thanks for the contribution! Fixing this issue is important from the security PoV, as the server should manage user sessions, not the clients. |
|
@kkovaletp everything is done and ready for review |
|
|
||
| // Find the auth-token from the context. REQUIRES AuthCookieSetter to have run. | ||
| func ResolverCookieFromContext(ctx context.Context) *string { | ||
| raw, _ := ctx.Value(userAccessTokenCtxKey).(*string) |
There was a problem hiding this comment.
This may panic if userAccessTokenCtxKey isn't set correctly. How about this:
v := ctx.Value(userAccessTokenCtxKey)
if v == nil {
return nil
}
return v.(*string)
There was a problem hiding this comment.
If we return nil, then won't we just panic in the AuthorizeUseror InitialSetupWizard resolver anyway?
https://github.com/photoview/photoview/pull/1387/changes#diff-4c4a03d7186972110cd0c98c0f259cc92bee167d3fabff79277ebf45b9f20b2eR51
I'm sort of assuming if you're calling this function, then the AuthCookieSetter middleware has run. I see it as the same assumption UserFromContext makes just a couple lines up about the auth middleware having run.
There was a problem hiding this comment.
BTW, I'd be happy to change them both (ResolverCookieFromContext and UserFromContext)
There was a problem hiding this comment.
Since this function is far away from where the middleware is registered, it's hard to ensure the middleware exists. This detection can output more specific relevant logs when unexpected events occur.
| // was provided by a resolver and the cookie has not already been written. | ||
| // For cross-origin deployments (separateDomain=true), it uses SameSite=None;Secure. | ||
| func (w *authResponseWriter) writeAuthCookieIfNeeded() { | ||
| if w.cookieWritten == false && w.authTokenFromResolver != "" { |
There was a problem hiding this comment.
It's better to shortcut the bad case first.
if w.cookieWritten || w.authTokenFromResolver == "" {
return
}
// other code| // It also embeds http.Hijacker to support WebSocket upgrades. | ||
| type authResponseWriter struct { | ||
| http.ResponseWriter | ||
| http.Hijacker |
There was a problem hiding this comment.
Well, actually, Hijacker is not enough. There are Pusher and Flusher, even deprecated CloseNotifier.
There is no good way to solve it. Could you use httpsnoop library to add hooks?
There was a problem hiding this comment.
Updated. Thanks for pointing me to httpsnoop, I did not know that exists and seems like the best solution to the nasty problem of the underlying responsewriter implementing all sorts of different interfaces..
| http.SetCookie(w, &http.Cookie{ | ||
| Name: "auth-token", | ||
| Value: w.authTokenFromResolver, | ||
| Path: "/", | ||
| SameSite: sameSite, | ||
| Secure: secure, | ||
| Expires: time.Now().Add(14 * 24 * time.Hour), | ||
| }) |
Check warning
Code scanning / CodeQL
Cookie 'HttpOnly' attribute is not set to true Medium
There was a problem hiding this comment.
Out of scope of this PR - this is the current behavior when the cookie is set on the front end. The front end currently reads the cookie to tell if the user is logged in. This can be updated in a separate PR.
| http.SetCookie(w, &http.Cookie{ | ||
| Name: "auth-token", | ||
| Value: w.authTokenFromResolver, | ||
| Path: "/", | ||
| SameSite: sameSite, | ||
| Secure: secure, | ||
| Expires: time.Now().Add(14 * 24 * time.Hour), | ||
| }) |
Check warning
Code scanning / CodeQL
Cookie 'Secure' attribute is not set to true Medium
There was a problem hiding this comment.
It is set to true if PHOTOVIEW_UI_ENDPOINT is set. In the case it is not, then the ui and api must be on the same domain. I do not want to break someones private network deployment that is maybe just served over http. I am assuming this is a pretty common setup.
googollee
left a comment
There was a problem hiding this comment.
The backend part is good.
@kkovaletp do you have any other idea?
|
@tyler-hagen I built an image based on your PR, The old behavior will redirect to the login page and remove the token from cookies. This PR breaks this behavior. Could you try to fix it? Thanks. |
|
@tyler-hagen after some checking, the unauthorized issue is not related with this pr. It still exists in the master code. The backend is good. |
There was a problem hiding this comment.
@coderabbitai, this comment is related to all changes in this PR, not only in this file.
Please carefully analyze this PR in the context of the project code, configuration, and documentation available in this repo. Please take into account all possible and supported deployment types (including single-image Docker, separate images with API and UI running in different containers, direct deployment on the host, a deployment behind a proxy, etc.). Please make sure to resolve all assumptions based on the info from the repo and post your investigation results after you're confident in them. Please take this chance of an end-to-end PR review from different angles as the final check before the merge.
Based on your analysis, please answer:
- Is this PR consistent with the product code?
- Does it implement the functionality in the most efficient and reliable way?
- Do you see any major (or higher) risks, bugs, or regressions preventing it from being merged?
There was a problem hiding this comment.
🧩 Analysis chain
This is a thorough request — let me dig into the repository to gather all the context needed before giving you a confident answer.
🏁 Script executed:
#!/bin/bash
echo "=== api/server.go ==="
cat api/server.go
echo ""
echo "=== api/graphql/auth/auth.go ==="
cat api/graphql/auth/auth.go
echo ""
echo "=== api/graphql/auth/auth_cookie_setter.go ==="
cat api/graphql/auth/auth_cookie_setter.go
echo ""
echo "=== api/graphql/resolvers/user.go ==="
cat api/graphql/resolvers/user.go
echo ""
echo "=== ui/src/Pages/LoginPage/loginUtilities.tsx ==="
cat ui/src/Pages/LoginPage/loginUtilities.tsx
echo ""
echo "=== ui/src/Pages/LoginPage/LoginPage.tsx ==="
cat ui/src/Pages/LoginPage/LoginPage.tsx
echo ""
echo "=== ui/src/Pages/LoginPage/InitialSetupPage.tsx ==="
cat ui/src/Pages/LoginPage/InitialSetupPage.tsx
echo ""
echo "=== Environment variable definitions ==="
rg -rn "PHOTOVIEW_SERVE_UI|SEPARATE_DOMAIN|separateDomain|SeparateDomain|separate.domain|separate_domain" --type=go --type=ts --type=yaml --type=env -i
echo ""
echo "=== Existing cookie handling in api ==="
rg -rn "SetCookie\|auth.token\|auth-token\|saveTokenCookie\|token.*cookie\|cookie.*token" --type=go -i
echo ""
echo "=== CORS configuration ==="
rg -rn "CORS\|cors\|SameSite\|SameSiteNone" --type=go -C3
echo ""
echo "=== docker-compose files ==="
fd "docker-compose" --exec cat {}
echo ""
echo "=== Dockerfile(s) ==="
fd "Dockerfile" --exec cat {}
echo ""
echo "=== Environment variable loading in api ==="
fd "*.go" --exec rg -l "PHOTOVIEW" {} \; 2>/dev/null | head -30
rg -rn "PHOTOVIEW" --type=go -C1 | head -100
echo ""
echo "=== Proxy / nginx config ==="
fd "nginx" --exec cat {}
echo ""
echo "=== Existing saveTokenCookie and related UI ==="
rg -rn "saveTokenCookie\|auth.token\|auth-token" --type=ts -C3
echo ""
echo "=== ui/src/Pages/LoginPage ==="
ls ui/src/Pages/LoginPage/
echo ""
echo "=== Auth cookie setter test ==="
cat api/graphql/auth/auth_cookie_setter_test.go
Length of output: 61959
Let me gather a bit more context before giving the final answer.
🏁 Script executed:
#!/bin/bash
echo "=== api/utils/environment_variables.go ==="
cat api/utils/environment_variables.go
echo ""
echo "=== api/utils/urls.go or similar ==="
fd "url" api/utils/ --exec cat {}
echo ""
echo "=== ui/src/helpers/authentication.tsx ==="
cat ui/src/helpers/authentication.tsx
echo ""
echo "=== Access token model (expiry) ==="
fd "access_token" --type=f --exec cat {}
echo ""
echo "=== CORS middleware ==="
cat api/server/cors.go 2>/dev/null || fd "cors" --type=f --exec cat {}
echo ""
echo "=== server/logging.go (LoggingMiddleware) ==="
cat api/server/logging.go 2>/dev/null || fd "logging" api/server/ --exec cat {}
echo ""
echo "=== saveTokenCookie usages in UI ==="
rg -rn "saveTokenCookie\|authToken\|auth-token\|auth_token" --type=ts -C2
echo ""
echo "=== LoginPage tests ==="
cat ui/src/Pages/LoginPage/LoginPage.test.tsx
cat ui/src/Pages/LoginPage/InitialSetupPage.test.tsx
echo ""
echo "=== loginTestHelpers ==="
cat ui/src/Pages/LoginPage/loginTestHelpers.ts
echo ""
echo "=== httpsnoop in go.mod ==="
grep -i "httpsnoop\|felixge" api/go.mod api/go.sum | head -10
echo ""
echo "=== Check for nil deref - ResolverCookieFromContext usage ==="
rg -rn "ResolverCookieFromContext" --type=go -C3
echo ""
echo "=== PHOTOVIEW_UI_ENDPOINT(S) in docker-compose ==="
rg -rn "UI_ENDPOINT" --type=yaml -C2
echo ""
echo "=== PHOTOVIEW_UI_ENDPOINT(S) in .env example files ==="
fd ".env" --exec cat {} 2>/dev/null | head -100
fd "env.example\|env.sample\|\.env\.example" --exec cat {} 2>/dev/null | head -100
Length of output: 20160
Let me check the logout flow and the authentication helper before giving the final answer.
🏁 Script executed:
#!/bin/bash
echo "=== Logout resolver ==="
rg -rn "logout\|Logout\|SignOut\|signout" --type=go -C5 | head -80
echo ""
echo "=== ui/src/helpers/authentication.ts ==="
fd "authentication" ui/src/ --exec cat {}
echo ""
echo "=== Access token model ==="
fd "access_token\|AccessToken" api/ --type=f --exec cat {}
echo ""
echo "=== utils urls ==="
fd "url" api/utils/ --type=f --exec cat {}
echo ""
echo "=== What does UiEndpointUrl do ==="
rg -rn "UiEndpointUrl\|UIEndpoint\|ApiEndpointUrl\|ApiListenUrl" --type=go -C5 | head -60
echo ""
echo "=== Is there a logout cookie clear in any other handler ==="
rg -rn "MaxAge\|SetCookie\|Delete.*[Cc]ookie\|cookie.*[Dd]elete\|[Ee]xpire.*[Cc]ookie" --type=go -C3
echo ""
echo "=== Check PHOTOVIEW_UI_ENDPOINTS usage in entire codebase ==="
rg -rn "UI_ENDPOINTS\|UIEndpoints\|uiEndpoints" --type=go --type=ts -C3
rg -rn "UI_ENDPOINTS" -C3
echo ""
echo "=== Check how authToken is used in main app to check auth state ==="
rg -rn "authToken\|isAuthenticated\|auth.*[Cc]heck" --type=ts -C3 | head -80
Length of output: 6166
@kkovaletp — I've done a thorough end-to-end analysis of this PR, cross-referencing the middleware chain, deployment configurations, login/auth flows, and every changed file. Here are my findings.
End-to-End PR Review: #1387 "Set access token server side"
Is this PR consistent with the product code?
Mostly yes, with one concrete inconsistency that needs fixing:
- The PR correctly reuses the existing
PHOTOVIEW_UI_ENDPOINTenv var (EnvUIEndpoint) — the same one already consulted by the CORS middleware and the WebSocket handler to determine whether cross-domain mode is active. TheseparateDomainflag is derived cleanly fromutils.EnvUIEndpoint.GetValue() != ""inserver.go. - The
httpsnooplibrary (github.com/felixge/httpsnoop v1.0.4) was already a dependency ingo.mod, so no new transitive dependency is introduced. - The middleware chain order is correct:
dataloader.Middleware→auth.Middleware→server.LoggingMiddleware→auth.AuthCookieSetter→server.CORSMiddleware.AuthCookieSettermust run afterLoggingMiddleware(so that thestatusResponseWriterwrapping is already in place) and beforeCORSMiddleware(soAccess-Control-Allow-Credentials: trueis in the response before the cookie is written) — this ordering is correct. - The 14-day cookie expiry matches the
AUTH_TOKEN_MAX_AGE_IN_DAYS = 14constant already defined inui/src/helpers/authentication.ts, so both sides agree on the lifetime. - Inconsistency — orphaned import (compile/lint risk):
loginUtilities.tsxstill importssaveTokenCookiefromauthentication.tsbut never calls it after the refactoring:Depending on// loginUtilities.tsx — this import is now dead import { saveTokenCookie } from '../../helpers/authentication'
tsconfig/ESLint rules (no-unused-vars,@typescript-eslint/no-unused-vars), this will produce a build-time or CI lint error. It must be removed.
Does it implement the functionality in the most efficient and reliable way?
Efficient: yes. Reliable: not quite — one hard crash lurks.
The httpsnoop.Wrap approach (intercepting both WriteHeader and Write) is clean, idiomatic, and correctly handles both paths through which gqlgen can flush a response (explicit WriteHeader(200) vs. implicit on first Write). The test suite covers both paths. The pointer-in-context pattern (context.WithValue(ctx, key, &authToken)) is an unconventional but functional way to let a downstream resolver "write back" to the middleware.
However: both AuthorizeUser and InitialSetupWizard in user.go dereference the pointer without a nil guard:
cookie := auth.ResolverCookieFromContext(ctx)
*cookie = token.Value // panics if cookie == nilResolverCookieFromContext explicitly returns nil when the context key is absent:
func ResolverCookieFromContext(ctx context.Context) *string {
v := ctx.Value(userAccessTokenCtxKey)
if v == nil {
return nil // ← returns nil
}
return v.(*string)
}In production this is safe today because AuthCookieSetter is always registered. But any integration test, benchmark, or future code path that calls AuthorizeUser/InitialSetupWizard without going through the full server.go middleware chain will panic. The fix is a two-line guard in each resolver:
if cookie := auth.ResolverCookieFromContext(ctx); cookie != nil {
*cookie = token.Value
}Do you see any major risks, bugs, or regressions preventing merge?
🔴 Must-fix before merge
| # | File | Issue |
|---|---|---|
| 1 | api/graphql/resolvers/user.go |
Nil pointer dereference — *cookie = token.Value with no nil guard panics if middleware is absent (tests, future refactors). Fix: wrap in if cookie != nil. Both AuthorizeUser and InitialSetupWizard are affected. |
| 2 | ui/src/Pages/LoginPage/loginUtilities.tsx |
Dead import — import { saveTokenCookie } is never used after the refactor. Causes lint/TS error depending on project config. |
🟡 Known trade-offs (acknowledged out-of-scope, but confirm understanding)
3. Cross-domain: authToken() returns undefined after login
authToken() in authentication.ts reads from document.cookie via js-cookie. In a cross-domain deployment (UI on ui.example.com, API on api.example.com), the Set-Cookie header from the API sets the token on api.example.com's cookie jar. JavaScript running on ui.example.com cannot read that cookie. Therefore:
- After a successful login,
login()redirects to the UI's base URL ✓ - The home page's GraphQL requests succeed because the browser automatically sends the
auth-tokencookie to the API (SameSite=None; Secure ✓) - But if the user navigates back to
/login, theif (authToken()) navigate('/')guard doesn't fire (returnsundefined) — the user sees the login form again even though they're authenticated
This is a UX regression specifically for cross-domain setups, but not a functional auth failure. It was the pre-existing behavior for cross-domain anyway (nothing worked at all before this PR). Confirmed out-of-scope per the PR description.
4. Cross-domain: logout does not clear the API-domain cookie
The app has no server-side logout endpoint. Currently, logout is handled purely client-side by clearTokenCookie() in authentication.ts. For same-domain deployments this still works — the UI JS can remove a cookie on the same origin. For cross-domain deployments, clearTokenCookie() targets ui.example.com cookies and cannot reach the auth-token cookie set on api.example.com. After logout, the API-domain cookie persists until its 14-day expiry, meaning the browser will keep authenticating API requests after the user "logged out."
This is a security gap amplified (but not introduced) by this PR for cross-domain use. A proper server-side logout endpoint that responds with Set-Cookie: auth-token=; Max-Age=0; Path=/ is the correct fix. Recommend tracking as a follow-up issue.
Deployment matrix
| Deployment type | Login auth | Post-login UI state | Logout |
|---|---|---|---|
| Single-image Docker (API+UI same origin) | ✅ works | ✅ authToken() reads cookie |
✅ clearTokenCookie() works |
| Separate containers, same domain/path via proxy | ✅ works | ✅ authToken() reads cookie |
✅ clearTokenCookie() works |
Separate containers, different domains (PHOTOVIEW_UI_ENDPOINT set) |
✅ fixed by this PR | authToken() returns null |
|
| Behind reverse proxy (nginx, Traefik, Caddy) | ✅ works (headers pass through) | same as above based on same/diff domain | same as above |
| Direct host deployment | ✅ works | ✅ authToken() reads cookie |
✅ clearTokenCookie() works |
Summary
The PR correctly solves the core problem (issue #184) and is well-implemented. Two items must be fixed before merge: the nil-pointer dereference in both resolver functions and the dead saveTokenCookie import. The cross-domain authToken() and logout issues are real and worth tracking as follow-ups, but do not block the immediate fix.
Would you like me to open a follow-up GitHub issue for the server-side logout / cross-domain cookie clearing gap?
🧠 Learnings used
Learnt from: tyler-hagen
Repo: photoview/photoview PR: 1387
File: api/graphql/auth/auth_cookie_setter.go:9-13
Timestamp: 2026-03-05T08:02:59.889Z
Learning: In photoview/photoview (api/graphql/auth/auth_cookie_setter.go), the unconditional `w.(http.Hijacker)` type assertion is intentionally safe because `AuthCookieSetter` middleware is always registered after `LoggingMiddleware` in api/server.go. By the time `AuthCookieSetter` receives the ResponseWriter, it is already a `*statusResponseWriter` (from logging.go) which always implements `http.Hijacker`. Do not flag this assertion as a panic risk in future reviews of this middleware stack.
Learnt from: tyler-hagen
Repo: photoview/photoview PR: 1387
File: api/graphql/auth/auth_cookie_setter.go:0-0
Timestamp: 2026-03-05T07:19:38.996Z
Learning: When implementing an http.ResponseWriter wrapper for gqlgen (or similar HTTP handlers), ensure you override both WriteHeader and Write so headers/cookies are set consistently in all paths, including the successful response path. Do not rely on implicit WriteHeader(200) in the happy path; explicitly handle header/cookie transmission in both error and success cases to avoid subtle bugs. Apply this guideline to Go files involved in HTTP transport wrappers within the api/graphql directory (and nearby analogous HTTP wrapper implementations).
Learnt from: kkovaletp
Repo: photoview/photoview PR: 965
File: ui/src/helpers/testUtils.tsx:0-0
Timestamp: 2025-02-07T18:56:43.166Z
Learning: When reviewing code that was previously suggested by CodeRabbit, avoid suggesting changes unless there are critical issues. This prevents unnecessary iterations and maintains consistency in recommendations.
Learnt from: DanielOaks
Repo: photoview/photoview PR: 1016
File: ui/src/components/mapbox/mapboxHelperFunctions.tsx:101-115
Timestamp: 2025-04-13T05:38:45.180Z
Learning: Error handling improvements and code style changes should be addressed in dedicated PRs rather than bundled with dependency update PRs which should remain focused on minimal changes required for the updates.
Learnt from: DanielOaks
Repo: photoview/photoview PR: 1016
File: ui/src/components/mapbox/mapboxHelperFunctions.tsx:44-57
Timestamp: 2025-04-13T05:39:39.494Z
Learning: For Photoview PRs, feature enhancements like improved error logging should be suggested as separate PRs rather than being included in dependency update PRs, which should focus on minimal necessary code changes.
There was a problem hiding this comment.
@tyler-hagen, @googollee, please take a look at the bot's findings. Are they valid?
As for me, the missing logout issue is the most important thing, as it allows to steal a cookie and use it till the 14-day expiration is reached.
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
There was a problem hiding this comment.
I agree I actually think the logout issue is the biggest one despite it being listed under "trade offs"
But I think they are all important. I'll fix all 4 issues
|
I agree on:
if cookie := auth.ResolverCookieFromContext(ctx); cookie != nil {
*cookie = token.Value
}
For |
googollee
left a comment
There was a problem hiding this comment.
Please guard the return when using ResolverCookieFromContext().
|
Apologies for the late reply - I've been traveling but am back home now.
Agreed it seems pre-existing, I will try to narrow down the problem in a separate issue/PR. |
Fix for: #184
Auth should now work even if the ui and api are hosted on separate domains.
Further improvement would be to not return the auth token in the graphql response, and also set the cookie to HttpOnly to avoid XSS access to the token. I figured that is out of scope for this PR.
Summary by CodeRabbit
New Features
Changes
Tests