diff --git a/pygitweb/auth.py b/pygitweb/auth.py
index 4579c01..cb63480 100644
--- a/pygitweb/auth.py
+++ b/pygitweb/auth.py
@@ -39,7 +39,11 @@ LOGIN_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
 oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
 _access_token_cookie = APIKeyCookie(name=ACCESS_TOKEN_COOKIE_NAME, auto_error=False)
 
-auth_router = APIRouter(tags=["auth"])
+TAG_AUTH = "auth"
+TAG_AUTH_LOCAL = "auth - local"
+TAG_AUTH_OAUTH = "auth - OAuth"
+
+auth_router = APIRouter()
 auth_router.include_router(oauth_router)
 
 
@@ -126,7 +130,13 @@ def require_active_user_if_auth_enabled(
 	ensure_active_user_if_auth_enabled(token)
 
 
-@auth_router.get("/login", response_class=HTMLResponse)
+@auth_router.get(
+	"/login",
+	response_class=HTMLResponse,
+	tags=[TAG_AUTH],
+	summary="Sign-in page",
+	description="HTML sign-in page. Shows local and/or OAuth options depending on auth.json.",
+)
 async def login_page(
 	next: Annotated[str, Query()] = "/",
 ) -> HTMLResponse:
@@ -145,7 +155,14 @@ async def login_page(
 	return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
 
 
-@auth_router.post("/login", response_model=None)
+@auth_router.post(
+	"/login",
+	response_model=None,
+	tags=[TAG_AUTH_LOCAL],
+	summary="Local sign-in (form)",
+	description="Browser form login. Sets the pygitweb_access_token session cookie on success.",
+	responses={303: {"description": "Redirect to next URL with session cookie set"}},
+)
 async def login_form_submit(
 	username: Annotated[str, Form()],
 	password: Annotated[str, Form()],
@@ -181,7 +198,13 @@ async def login_form_submit(
 	return resp
 
 
-@auth_router.get("/logout")
+@auth_router.get(
+	"/logout",
+	tags=[TAG_AUTH],
+	summary="Sign out",
+	description="Revokes the current session (cookie or Bearer) and clears auth cookies.",
+	responses={303: {"description": "Redirect to next URL"}},
+)
 async def logout_page(
 	token: Annotated[str | None, Depends(access_token_from_request)],
 	next: Annotated[str, Query()] = "/",
@@ -193,7 +216,15 @@ async def logout_page(
 	return resp
 
 
-@auth_router.post("/token")
+@auth_router.post(
+	"/token",
+	tags=[TAG_AUTH_LOCAL],
+	summary="Obtain session token (local)",
+	description=(
+		"OAuth2 password flow for API clients. Returns an opaque session id as access_token; "
+		"use Authorization: Bearer on protected routes. Not an identity-provider JWT."
+	),
+)
 async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> dict[str, str]:
 	if not settings.AUTH:
 		raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Authentication is disabled")
@@ -206,11 +237,15 @@ async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> d
 	return {"access_token": sid, "token_type": "bearer"}
 
 
-@auth_router.get("/auth/status")
+@auth_router.get(
+	"/auth/status",
+	tags=[TAG_AUTH],
+	summary="Auth status (navbar)",
+	description="Lightweight JSON for the navbar script. Does not return 401 when anonymous.",
+)
 async def auth_status(
 	token: Annotated[str | None, Depends(access_token_from_request)],
 ) -> dict[str, bool | str | None]:
-	"""Lightweight JSON for the navbar script (no 401 when anonymous)."""
 	if not settings.AUTH:
 		return {"auth_enabled": False, "username": None}
 	if not token:
@@ -221,7 +256,14 @@ async def auth_status(
 	return {"auth_enabled": True, "username": u.username}
 
 
-@auth_router.get("/user", response_class=HTMLResponse, response_model=None)
+@auth_router.get(
+	"/user",
+	response_class=HTMLResponse,
+	response_model=None,
+	tags=[TAG_AUTH],
+	summary="Account page",
+	description="HTML account page for the signed-in user.",
+)
 async def user_account_page(
 	token: Annotated[str | None, Depends(access_token_from_request)],
 ) -> HTMLResponse | RedirectResponse:
@@ -242,7 +284,13 @@ async def user_account_page(
 	return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
 
 
-@auth_router.get("/users/me", response_model=User)
+@auth_router.get(
+	"/users/me",
+	response_model=User,
+	tags=[TAG_AUTH],
+	summary="Current user",
+	description="Returns the username for the current session (cookie or Bearer).",
+)
 async def read_users_me(
 	token: Annotated[str | None, Depends(access_token_from_request)],
 ) -> User:
diff --git a/pygitweb/auth_oauth.py b/pygitweb/auth_oauth.py
index f1b62ed..7818881 100644
--- a/pygitweb/auth_oauth.py
+++ b/pygitweb/auth_oauth.py
@@ -31,7 +31,9 @@ OAUTH_STATE_MAX_AGE = 600
 ACCESS_TOKEN_COOKIE_NAME = "pygitweb_access_token"
 LOGIN_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
 
-oauth_router = APIRouter(tags=["auth"])
+TAG_AUTH_OAUTH = "auth - OAuth"
+
+oauth_router = APIRouter()
 
 
 def _safe_next_url(next_raw: str | None) -> str:
@@ -228,7 +230,16 @@ def _profile_email(profile: dict[str, Any]) -> str | None:
 	return str(email) if email else None
 
 
-@oauth_router.get("/auth/oauth/start")
+@oauth_router.get(
+	"/auth/oauth/start",
+	tags=[TAG_AUTH_OAUTH],
+	summary="Start external OAuth sign-in",
+	description=(
+		"Browser-only. Redirects to the configured provider (Google or GitHub). "
+		"On success, /auth/oauth/callback sets the same pygitweb_access_token session cookie as local login."
+	),
+	responses={303: {"description": "Redirect to identity provider"}},
+)
 async def oauth_start(
 	next: Annotated[str, Query()] = "/",
 ) -> RedirectResponse:
@@ -269,7 +280,16 @@ async def oauth_start(
 	return resp
 
 
-@oauth_router.get("/auth/oauth/callback")
+@oauth_router.get(
+	"/auth/oauth/callback",
+	tags=[TAG_AUTH_OAUTH],
+	summary="OAuth provider callback",
+	description=(
+		"Browser-only. Called by the identity provider after sign-in. "
+		"Validates state, exchanges the code, and sets the session cookie. Not intended for Try it out."
+	),
+	responses={303: {"description": "Redirect to original next URL with session cookie set"}},
+)
 async def oauth_callback(
 	request: Request,
 	code: Annotated[str | None, Query()] = None,
diff --git a/pygitweb/main.py b/pygitweb/main.py
index bb1f87a..0335d87 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -198,12 +198,28 @@ with open("pygitweb/README.md", encoding="utf-8") as _readme_file:
 	_app_description = _readme_file.read()
 
 app = FastAPI(
-	debug=(not settings.AUTH),
+	debug=(settings.AUTH is False),
 	title="PyGitWeb",
 	summary="FastAPI + Pygit2 Repo Browser",
 	description=_app_description,
 	version=__meta__.__version__,
 	lifespan=app_lifespan,
+	openapi_tags=[
+		{
+			"name": "auth",
+			"description": """Session status, account pages, and sign-out.
+Sessions are opaque ids (cookie or Bearer), not IdP JWTs.""",
+		},
+		{
+			"name": "auth - local",
+			"description": "Local username/password sign-in via HTML form or POST /token.",
+		},
+		{
+			"name": "auth - OAuth",
+			"description": """Browser OAuth2 authorization-code flow (Google/GitHub).
+Ends with the same session cookie as local login.""",
+		},
+	],
 )
 
 PLUGIN_ACTIONS = load_plugin_actions()
