An OIDC-protected index page for your homeserver.
at main 188 lines 4.3 kB view raw
1package auth 2 3import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "log/slog" 9 "net/http" 10 "time" 11 12 badger "github.com/dgraph-io/badger/v4" 13 gonanoid "github.com/matoous/go-nanoid/v2" 14 "github.com/zitadel/logging" 15 "github.com/zitadel/oidc/v3/pkg/client/rp" 16 httphelper "github.com/zitadel/oidc/v3/pkg/http" 17 "github.com/zitadel/oidc/v3/pkg/oidc" 18) 19 20const SESSION_NAME = "_ladon_session" 21 22var ( 23 ErrNoSession = errors.New("ladon: no session cookie set") 24 ErrSessionExpired = errors.New("ladon: session expired") 25) 26 27func State() string { 28 return gonanoid.Must() 29} 30 31type AuthManager struct { 32 Db *badger.DB 33 Env *EnvConfig 34 CookieHandler *httphelper.CookieHandler 35 RelyingParty rp.RelyingParty 36 Log *slog.Logger 37 HttpClient *http.Client 38} 39 40func NewAuthManager(logger *slog.Logger) *AuthManager { 41 env := EnvMustParse() 42 43 db, err := badger.Open(badger.DefaultOptions("").WithInMemory(true)) 44 if err != nil { 45 panic(err) 46 } 47 48 cookieHandler := httphelper.NewCookieHandler(env.SessionSecret, env.SessionSecret, httphelper.WithUnsecure()) 49 50 client := &http.Client{ 51 Timeout: time.Minute, 52 } 53 54 options := []rp.Option{ 55 rp.WithCookieHandler(cookieHandler), 56 rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)), 57 rp.WithHTTPClient(client), 58 rp.WithLogger(logger), 59 rp.WithSigningAlgsFromDiscovery(), 60 } 61 62 logging.EnableHTTPClient(client, 63 logging.WithClientGroup("client"), 64 ) 65 66 ctx := logging.ToContext(context.TODO(), logger) 67 provider, err := rp.NewRelyingPartyOIDC( 68 ctx, 69 env.Issuer, 70 env.ClientID, 71 env.ClientSecret, 72 fmt.Sprintf("%s/callback", env.LadonHost), 73 []string{"openid profile"}, 74 options..., 75 ) 76 if err != nil { 77 logger.Error("ladon: failed to instantiate relying party client") 78 panic(err) 79 } 80 81 return &AuthManager{ 82 Db: db, 83 Env: env, 84 CookieHandler: cookieHandler, 85 RelyingParty: provider, 86 Log: logger, 87 HttpClient: client, 88 } 89} 90 91func (a *AuthManager) HandleLogin() http.Handler { 92 return rp.AuthURLHandler( 93 State, 94 a.RelyingParty, 95 ) 96} 97 98func (a *AuthManager) HandleCallback() http.Handler { 99 return rp.CodeExchangeHandler( 100 func( 101 w http.ResponseWriter, 102 r *http.Request, 103 tokens *oidc.Tokens[*oidc.IDTokenClaims], 104 state string, 105 rp rp.RelyingParty, 106 ) { 107 data, err := json.Marshal(tokens) 108 if err != nil { 109 http.Error(w, err.Error(), http.StatusInternalServerError) 110 return 111 } 112 113 id := tokens.IDTokenClaims.Subject 114 115 if err := a.Db.Update(func(txn *badger.Txn) error { 116 e := badger.NewEntry([]byte(id), data).WithTTL(time.Duration(tokens.ExpiresIn) * time.Second) 117 err := txn.SetEntry(e) 118 return err 119 }); err != nil { 120 http.Error(w, "failed to cache user session", http.StatusInternalServerError) 121 } 122 123 if err := a.CookieHandler.SetCookie(w, SESSION_NAME, id); err != nil { 124 http.Error(w, "failed to set session cookie", http.StatusInternalServerError) 125 return 126 } 127 128 w.Header().Add("Location", "/") 129 w.WriteHeader(http.StatusFound) 130 }, 131 a.RelyingParty, 132 ) 133} 134 135func (a *AuthManager) HandleLogout() http.Handler { 136 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 137 a.CookieHandler.DeleteCookie(w, SESSION_NAME) 138 139 w.Header().Add("Location", "/") 140 w.WriteHeader(http.StatusFound) 141 }) 142} 143 144func (a *AuthManager) GetSession(r *http.Request) (*oidc.IDTokenClaims, error) { 145 id, err := a.CookieHandler.CheckCookie(r, SESSION_NAME) 146 if errors.Is(err, http.ErrNoCookie) { 147 return nil, ErrNoSession 148 } else if err != nil { 149 return nil, err 150 } 151 152 tokens := &oidc.Tokens[*oidc.IDTokenClaims]{} 153 if err := a.Db.View(func(txn *badger.Txn) error { 154 item, err := txn.Get([]byte(id)) 155 if err != nil { 156 return err 157 } 158 159 err = item.Value(func(val []byte) error { 160 return json.Unmarshal(val, tokens) 161 }) 162 if err != nil { 163 return err 164 } 165 166 return nil 167 }); err != nil { 168 return nil, ErrNoSession 169 } 170 171 claims, err := rp.VerifyTokens[*oidc.IDTokenClaims]( 172 context.TODO(), 173 tokens.AccessToken, 174 tokens.IDToken, 175 a.RelyingParty.IDTokenVerifier(), 176 ) 177 if errors.Is(err, oidc.ErrExpired) { 178 return nil, ErrSessionExpired 179 } else if err != nil { 180 return nil, err 181 } 182 183 return claims, nil 184} 185 186func (a *AuthManager) DeleteSession(w http.ResponseWriter) { 187 a.CookieHandler.DeleteCookie(w, SESSION_NAME) 188}