Gate searxng, notes, and hasura behind admin auth via nginx auth_request
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled

Add ValidateAdmin endpoint that checks JWT admin claim for use as an
nginx auth_request subrequest. Widen cookie path from backend endpoint
to "/" so the access_token is sent on all paths. Extend access token
lifetime from 24h to 7 days. Disable hasura service by default via
Docker profile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-12 21:33:41 +01:00
parent ee97ec9b23
commit d344497393
7 changed files with 92 additions and 16 deletions

View File

@@ -44,7 +44,7 @@ func (r *mutationResolver) Login(ctx context.Context, input model.LoginInput) (*
"access_token", "access_token",
tokens.AccessToken, tokens.AccessToken,
int(r.Store.Auth.Config.AccessTokenLifetime.Seconds()), int(r.Store.Auth.Config.AccessTokenLifetime.Seconds()),
r.Store.Auth.Config.Endpoint, "/",
r.Store.Auth.Config.Domain, r.Store.Auth.Config.Domain,
true, true, true, true,
) )
@@ -52,7 +52,7 @@ func (r *mutationResolver) Login(ctx context.Context, input model.LoginInput) (*
"refresh_token", "refresh_token",
tokens.RefreshToken, tokens.RefreshToken,
int(r.Store.Auth.Config.RefreshTokenLifetime.Seconds()), int(r.Store.Auth.Config.RefreshTokenLifetime.Seconds()),
r.Store.Auth.Config.Endpoint, "/",
r.Store.Auth.Config.Domain, r.Store.Auth.Config.Domain,
true, true, true, true,
) )
@@ -112,7 +112,7 @@ func (r *mutationResolver) RefreshToken(ctx context.Context) (*model.AuthPayload
"access_token", "access_token",
tokens.AccessToken, tokens.AccessToken,
int(r.Store.Auth.Config.AccessTokenLifetime.Seconds()), int(r.Store.Auth.Config.AccessTokenLifetime.Seconds()),
r.Store.Auth.Config.Endpoint, "/",
r.Store.Auth.Config.Domain, r.Store.Auth.Config.Domain,
true, true, true, true,
) )
@@ -120,7 +120,7 @@ func (r *mutationResolver) RefreshToken(ctx context.Context) (*model.AuthPayload
"refresh_token", "refresh_token",
tokens.RefreshToken, tokens.RefreshToken,
int(r.Store.Auth.Config.RefreshTokenLifetime.Seconds()), int(r.Store.Auth.Config.RefreshTokenLifetime.Seconds()),
r.Store.Auth.Config.Endpoint, "/",
r.Store.Auth.Config.Domain, r.Store.Auth.Config.Domain,
true, true, true, true,
) )

View File

@@ -50,6 +50,28 @@ func (store *Store) AdminMiddleware(ctx *gin.Context) {
ctx.Next() ctx.Next()
} }
func (store *Store) ValidateAdmin(ctx *gin.Context) {
accessToken, err := ctx.Cookie("access_token")
if err != nil {
ctx.Status(http.StatusUnauthorized)
return
}
claims, err := store.Auth.VerifyJWT(accessToken)
if err != nil {
ctx.Status(http.StatusUnauthorized)
return
}
admin, ok := (*claims)["admin"].(bool)
if !ok || !admin {
ctx.Status(http.StatusForbidden)
return
}
ctx.Status(http.StatusOK)
}
func (store *Store) CheckToken(ctx *gin.Context) { func (store *Store) CheckToken(ctx *gin.Context) {
access_token, err := ctx.Cookie("access_token") access_token, err := ctx.Cookie("access_token")
if err != nil { if err != nil {
@@ -123,7 +145,7 @@ func (store *Store) RefreshToken(ctx *gin.Context) {
"access_token", "access_token",
tokens.AccessToken, tokens.AccessToken,
int(store.Auth.Config.AccessTokenLifetime.Seconds()), int(store.Auth.Config.AccessTokenLifetime.Seconds()),
store.Auth.Config.Endpoint, "/",
store.Auth.Config.Domain, store.Auth.Config.Domain,
true, true, true, true,
) )
@@ -131,7 +153,7 @@ func (store *Store) RefreshToken(ctx *gin.Context) {
"refresh_token", "refresh_token",
tokens.RefreshToken, tokens.RefreshToken,
int(store.Auth.Config.RefreshTokenLifetime.Seconds()), int(store.Auth.Config.RefreshTokenLifetime.Seconds()),
store.Auth.Config.Endpoint, "/",
store.Auth.Config.Domain, store.Auth.Config.Domain,
true, true, true, true,
) )
@@ -169,7 +191,7 @@ func (store *Store) Login(ctx *gin.Context) {
"access_token", "access_token",
tokens.AccessToken, tokens.AccessToken,
int(store.Auth.Config.AccessTokenLifetime.Seconds()), int(store.Auth.Config.AccessTokenLifetime.Seconds()),
store.Auth.Config.Endpoint, "/",
store.Auth.Config.Domain, store.Auth.Config.Domain,
true, true, true, true,
) )
@@ -177,7 +199,7 @@ func (store *Store) Login(ctx *gin.Context) {
"refresh_token", "refresh_token",
tokens.RefreshToken, tokens.RefreshToken,
int(store.Auth.Config.RefreshTokenLifetime.Seconds()), int(store.Auth.Config.RefreshTokenLifetime.Seconds()),
store.Auth.Config.Endpoint, "/",
store.Auth.Config.Domain, store.Auth.Config.Domain,
true, true, true, true,
) )
@@ -197,7 +219,7 @@ func (store *Store) removeCookies(ctx *gin.Context) {
"access_token", "access_token",
"", "",
-1, -1,
store.Auth.Config.Endpoint, "/",
store.Auth.Config.Domain, store.Auth.Config.Domain,
true, true, true, true,
) )
@@ -205,7 +227,7 @@ func (store *Store) removeCookies(ctx *gin.Context) {
"refresh_token", "refresh_token",
"", "",
-1, -1,
store.Auth.Config.Endpoint, "/",
store.Auth.Config.Domain, store.Auth.Config.Domain,
true, true, true, true,
) )

View File

@@ -173,7 +173,7 @@ func (store *Store) DeleteUser(ctx *gin.Context) {
"access_token", "access_token",
"", "",
-1, -1,
store.Auth.Config.Endpoint, "/",
store.Auth.Config.Domain, store.Auth.Config.Domain,
true, true, true, true,
) )
@@ -181,7 +181,7 @@ func (store *Store) DeleteUser(ctx *gin.Context) {
"refresh_token", "refresh_token",
"", "",
-1, -1,
store.Auth.Config.Endpoint, "/",
store.Auth.Config.Domain, store.Auth.Config.Domain,
true, true, true, true,
) )

View File

@@ -70,7 +70,7 @@ func main() {
authSecret := os.Getenv("BACKEND_SECRET") authSecret := os.Getenv("BACKEND_SECRET")
backendEndpoint := os.Getenv("BACKEND_ENDPOINT") backendEndpoint := os.Getenv("BACKEND_ENDPOINT")
accessTokenLifetime := 24 * time.Hour accessTokenLifetime := 7 * 24 * time.Hour
refreshTokenLifetime := 365 * 24 * time.Hour refreshTokenLifetime := 365 * 24 * time.Hour
authConfig := services.AuthConfig{Secret: []byte(authSecret), Domain: domainName, RefreshTokenLifetime: refreshTokenLifetime, AccessTokenLifetime: accessTokenLifetime, Endpoint: backendEndpoint} authConfig := services.AuthConfig{Secret: []byte(authSecret), Domain: domainName, RefreshTokenLifetime: refreshTokenLifetime, AccessTokenLifetime: accessTokenLifetime, Endpoint: backendEndpoint}
auth := services.InitAuth(&authConfig) auth := services.InitAuth(&authConfig)
@@ -122,6 +122,7 @@ func main() {
r.POST("/auth/refresh", store.RefreshToken) r.POST("/auth/refresh", store.RefreshToken)
r.GET("/auth/check", store.CheckToken) r.GET("/auth/check", store.CheckToken)
r.POST("/auth/logout", store.Logout) r.POST("/auth/logout", store.Logout)
r.GET("/auth/validate-admin", store.ValidateAdmin)
// SPOTIFY // SPOTIFY
r.GET("/spotify/callback", store.CompleteSpotifyAuth) r.GET("/spotify/callback", store.CompleteSpotifyAuth)

View File

@@ -35,7 +35,6 @@ services:
- backend - backend
- icecast2 - icecast2
- gitea - gitea
- hasura
- quartz - quartz
- searxng - searxng
networks: networks:
@@ -96,6 +95,8 @@ services:
image: hasura/graphql-engine:v2.44.0 image: hasura/graphql-engine:v2.44.0
container_name: "${HASURA_HOST}" container_name: "${HASURA_HOST}"
restart: always restart: always
profiles:
- disabled
depends_on: depends_on:
- db - db
networks: networks:
@@ -135,7 +136,6 @@ services:
volumes: volumes:
- ${OBSIDIAN_DIR}:/quartz/content:ro - ${OBSIDIAN_DIR}:/quartz/content:ro
searxng: searxng:
build: build:
context: ./searxng context: ./searxng
@@ -151,7 +151,6 @@ services:
volumes: volumes:
- searxng_data:/etc/searxng - searxng_data:/etc/searxng
gitea: gitea:
image: docker.gitea.com/gitea:1.25.4-rootless image: docker.gitea.com/gitea:1.25.4-rootless
container_name: "${GITEA_HOST}" container_name: "${GITEA_HOST}"

View File

@@ -207,6 +207,8 @@ http {
} }
location /hasura/ { location /hasura/ {
auth_request /internal/auth/admin-validate;
error_page 401 403 = @auth_denied;
proxy_pass http://$HASURA_HOST:$HASURA_PORT/; proxy_pass http://$HASURA_HOST:$HASURA_PORT/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
@@ -222,6 +224,8 @@ http {
} }
location /notes/ { location /notes/ {
auth_request /internal/auth/admin-validate;
error_page 401 403 = @auth_denied;
proxy_pass http://$QUARTZ_HOST:$QUARTZ_PORT/; proxy_pass http://$QUARTZ_HOST:$QUARTZ_PORT/;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
@@ -233,11 +237,25 @@ http {
} }
location = /internal/auth/admin-validate {
internal;
proxy_pass http://$BACKEND_HOST:$BACKEND_PORT/auth/validate-admin;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Cookie $http_cookie;
}
location @auth_denied {
return 302 /;
}
location /searxng { location /searxng {
return 301 /searxng/; return 301 /searxng/;
} }
location /searxng/ { location /searxng/ {
auth_request /internal/auth/admin-validate;
error_page 401 403 = @auth_denied;
proxy_pass http://$SEARXNG_HOST:$SEARXNG_PORT/; proxy_pass http://$SEARXNG_HOST:$SEARXNG_PORT/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;

View File

@@ -134,6 +134,8 @@ http {
} }
location /hasura/ { location /hasura/ {
auth_request /internal/auth/admin-validate;
error_page 401 403 = @auth_denied;
proxy_pass http://$HASURA_HOST:$HASURA_PORT/; proxy_pass http://$HASURA_HOST:$HASURA_PORT/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
@@ -149,6 +151,8 @@ http {
} }
location /notes/ { location /notes/ {
auth_request /internal/auth/admin-validate;
error_page 401 403 = @auth_denied;
proxy_pass http://$QUARTZ_HOST:$QUARTZ_PORT/; proxy_pass http://$QUARTZ_HOST:$QUARTZ_PORT/;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
@@ -160,11 +164,25 @@ http {
} }
location = /internal/auth/admin-validate {
internal;
proxy_pass http://$BACKEND_HOST:$BACKEND_PORT/auth/validate-admin;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Cookie $http_cookie;
}
location @auth_denied {
return 302 /;
}
location /searxng { location /searxng {
return 301 /searxng/; return 301 /searxng/;
} }
location /searxng/ { location /searxng/ {
auth_request /internal/auth/admin-validate;
error_page 401 403 = @auth_denied;
proxy_pass http://$SEARXNG_HOST:$SEARXNG_PORT/; proxy_pass http://$SEARXNG_HOST:$SEARXNG_PORT/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
@@ -272,6 +290,8 @@ http {
} }
location /hasura/ { location /hasura/ {
auth_request /internal/auth/admin-validate;
error_page 401 403 = @auth_denied;
proxy_pass http://$HASURA_HOST:$HASURA_PORT/; proxy_pass http://$HASURA_HOST:$HASURA_PORT/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
@@ -287,6 +307,8 @@ http {
} }
location /notes/ { location /notes/ {
auth_request /internal/auth/admin-validate;
error_page 401 403 = @auth_denied;
proxy_pass http://$QUARTZ_HOST:$QUARTZ_PORT/; proxy_pass http://$QUARTZ_HOST:$QUARTZ_PORT/;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
@@ -298,11 +320,25 @@ http {
} }
location = /internal/auth/admin-validate {
internal;
proxy_pass http://$BACKEND_HOST:$BACKEND_PORT/auth/validate-admin;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Cookie $http_cookie;
}
location @auth_denied {
return 302 /;
}
location /searxng { location /searxng {
return 301 /searxng/; return 301 /searxng/;
} }
location /searxng/ { location /searxng/ {
auth_request /internal/auth/admin-validate;
error_page 401 403 = @auth_denied;
proxy_pass http://$SEARXNG_HOST:$SEARXNG_PORT/; proxy_pass http://$SEARXNG_HOST:$SEARXNG_PORT/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;