Migration This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs. Contact me

internal/authn/authn.go

554bcfc63721b22faf8bca78d5518d76e24a4d20 · 2.7 KB · 79 lines raw

1 // Package authn owns gsg's browser session endpoints. Login trusts the
2 // identity header that the reverse proxy sets after forward_auth; logout
3 // only clears gsg's own cookie. Keeping both here means main.go composes
4 // listeners rather than implementing authentication.
5 //
6 // The two handlers are returned separately on purpose: login must only ever
7 // be mounted on the admin listener, while logout belongs on the public one.
8 // A single combined handler would make that distinction a calling
9 // convention instead of a structural one.
10 package authn
11
12 import (
13 "fmt"
14 "net/http"
15 "net/url"
16 "strings"
17
18 "gsg/internal/session"
19 )
20
21 // Config is the small amount of state the session endpoints need.
22 type Config struct {
23 Sessions *session.Manager
24 UserHeader string // e.g. "X-Authentik-Username"; defaults to that
25 }
26
27 // Handlers are mounted individually by the caller.
28 type Handlers struct {
29 Login http.Handler
30 Logout http.Handler
31 }
32
33 // NewHandlers builds the login and logout endpoints.
34 func NewHandlers(cfg Config) (Handlers, error) {
35 if cfg.Sessions == nil {
36 return Handlers{}, fmt.Errorf("authn: no session manager")
37 }
38 if cfg.UserHeader == "" {
39 cfg.UserHeader = "X-Authentik-Username"
40 }
41 return Handlers{
42 Login: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { login(cfg, w, r) }),
43 Logout: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logout(cfg, w, r) }),
44 }, nil
45 }
46
47 // login mints a session from the proxy-supplied identity. Reaching this
48 // handler at all is the authorization decision: the admin listener is the
49 // only place it is mounted, and Caddy's forward_auth runs before it.
50 func login(cfg Config, w http.ResponseWriter, r *http.Request) {
51 user := r.Header.Get(cfg.UserHeader)
52 if user == "" {
53 http.Error(w, "missing identity header", http.StatusUnauthorized)
54 return
55 }
56 http.SetCookie(w, cfg.Sessions.Issue(user))
57 http.Redirect(w, r, safeNext(r.URL.Query().Get("next")), http.StatusSeeOther)
58 }
59
60 // logout needs no round trip through the identity provider; it only clears
61 // the cookie this process issued.
62 func logout(cfg Config, w http.ResponseWriter, r *http.Request) {
63 http.SetCookie(w, cfg.Sessions.Clear())
64 http.Redirect(w, r, "/", http.StatusSeeOther)
65 }
66
67 // safeNext returns next if it is a same-site relative path, else "/". next
68 // comes from a query parameter on a login redirect; without this check it
69 // would be an open redirect (?next=https://evil.example) off a trusted
70 // domain, right after proving who the user is.
71 func safeNext(next string) string {
72 if next == "" || !strings.HasPrefix(next, "/") || strings.HasPrefix(next, "//") {
73 return "/"
74 }
75 if u, err := url.Parse(next); err != nil || u.Scheme != "" || u.Host != "" {
76 return "/"
77 }
78 return next
79 }