added logout
This commit is contained in:
@@ -28,11 +28,9 @@ func Callback(
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
// Track callback redirect attempts
|
||||
attempts, exceeded, track := store.TrackRedirect(r, "/callback", 5)
|
||||
|
||||
if exceeded {
|
||||
// Build detailed error for logging
|
||||
err := errors.Errorf(
|
||||
"callback redirect loop detected after %d attempts | ip=%s ua=%s path=%s first_seen=%s",
|
||||
attempts,
|
||||
@@ -42,10 +40,8 @@ func Callback(
|
||||
track.FirstSeen.Format("2006-01-02T15:04:05Z07:00"),
|
||||
)
|
||||
|
||||
// Clear the tracking entry
|
||||
store.ClearRedirectTrack(r, "/callback")
|
||||
|
||||
// Show error page
|
||||
throwError(
|
||||
server,
|
||||
w,
|
||||
@@ -66,23 +62,17 @@ func Callback(
|
||||
}
|
||||
data, err := verifyState(cfg.OAuth, w, r, state)
|
||||
if err != nil {
|
||||
// Check if this is a cookie error (401) or signature error (403)
|
||||
if vsErr, ok := err.(*verifyStateError); ok {
|
||||
if vsErr.IsCookieError() {
|
||||
// Cookie missing/expired - normal failed/expired session (DEBUG)
|
||||
throwUnauthorized(server, w, r, "OAuth session not found or expired", err)
|
||||
} else {
|
||||
// Signature verification failed - security violation (WARN)
|
||||
throwForbiddenSecurity(server, w, r, "OAuth state verification failed", err)
|
||||
}
|
||||
} else {
|
||||
// Unknown error type - treat as security issue
|
||||
throwForbiddenSecurity(server, w, r, "OAuth state verification failed", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
// SUCCESS POINT: State verified successfully
|
||||
// Clear redirect tracking - OAuth callback completed successfully
|
||||
store.ClearRedirectTrack(r, "/callback")
|
||||
|
||||
switch data {
|
||||
@@ -108,10 +98,9 @@ func Callback(
|
||||
)
|
||||
}
|
||||
|
||||
// verifyStateError wraps an error with context about what went wrong
|
||||
type verifyStateError struct {
|
||||
err error
|
||||
cookieError bool // true if cookie missing/invalid, false if signature invalid
|
||||
cookieError bool
|
||||
}
|
||||
|
||||
func (e *verifyStateError) Error() string {
|
||||
@@ -135,20 +124,16 @@ func verifyState(
|
||||
return "", errors.New("state param field is empty")
|
||||
}
|
||||
|
||||
// Try to get the cookie
|
||||
uak, err := oauth.GetStateCookie(r)
|
||||
if err != nil {
|
||||
// Cookie missing or invalid - this is a 401 (not authenticated)
|
||||
return "", &verifyStateError{
|
||||
err: errors.Wrap(err, "oauth.GetStateCookie"),
|
||||
cookieError: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the state signature
|
||||
data, err := oauth.VerifyState(cfg, state, uak)
|
||||
if err != nil {
|
||||
// Signature verification failed - this is a 403 (security violation)
|
||||
return "", &verifyStateError{
|
||||
err: errors.Wrap(err, "oauth.VerifyState"),
|
||||
cookieError: false,
|
||||
@@ -170,9 +155,9 @@ func login(
|
||||
store *store.Store,
|
||||
discordAPI *discord.APIClient,
|
||||
) (func(), error) {
|
||||
token, err := discord.AuthorizeWithCode(cfg.Discord, code, cfg.HWSAuth.TrustedHost, discordAPI)
|
||||
token, err := discordAPI.AuthorizeWithCode(code)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "discord.AuthorizeWithCode")
|
||||
return nil, errors.Wrap(err, "discordAPI.AuthorizeWithCode")
|
||||
}
|
||||
session, err := discord.NewOAuthSession(token)
|
||||
if err != nil {
|
||||
@@ -204,6 +189,10 @@ func login(
|
||||
})
|
||||
redirect = "/register"
|
||||
} else {
|
||||
err = user.UpdateDiscordToken(ctx, tx, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "user.UpdateDiscordToken")
|
||||
}
|
||||
err := auth.Login(w, r, user, true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "auth.Login")
|
||||
|
||||
45
internal/handlers/isusernameunique.go
Normal file
45
internal/handlers/isusernameunique.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.haelnorr.com/h/golib/hws"
|
||||
"git.haelnorr.com/h/oslstats/internal/config"
|
||||
"git.haelnorr.com/h/oslstats/internal/db"
|
||||
"git.haelnorr.com/h/oslstats/internal/store"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func IsUsernameUnique(
|
||||
server *hws.Server,
|
||||
conn *bun.DB,
|
||||
cfg *config.Config,
|
||||
store *store.Store,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.FormValue("username")
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
throwInternalServiceError(server, w, r, "Database transaction failed", err)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
unique, err := db.IsUsernameUnique(ctx, tx, username)
|
||||
if err != nil {
|
||||
throwInternalServiceError(server, w, r, "Database query failed", err)
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
if !unique {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -17,11 +17,9 @@ func Login(server *hws.Server, cfg *config.Config, st *store.Store, discordAPI *
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
cookies.SetPageFrom(w, r, cfg.HWSAuth.TrustedHost)
|
||||
// Track login redirect attempts
|
||||
attempts, exceeded, track := st.TrackRedirect(r, "/login", 5)
|
||||
|
||||
if exceeded {
|
||||
// Build detailed error for logging
|
||||
err := errors.Errorf(
|
||||
"login redirect loop detected after %d attempts | ip=%s ua=%s path=%s first_seen=%s",
|
||||
attempts,
|
||||
@@ -31,10 +29,8 @@ func Login(server *hws.Server, cfg *config.Config, st *store.Store, discordAPI *
|
||||
track.FirstSeen.Format("2006-01-02T15:04:05Z07:00"),
|
||||
)
|
||||
|
||||
// Clear the tracking entry
|
||||
st.ClearRedirectTrack(r, "/login")
|
||||
|
||||
// Show error page
|
||||
throwError(
|
||||
server,
|
||||
w,
|
||||
@@ -54,14 +50,11 @@ func Login(server *hws.Server, cfg *config.Config, st *store.Store, discordAPI *
|
||||
}
|
||||
oauth.SetStateCookie(w, uak, cfg.HWSAuth.SSL)
|
||||
|
||||
link, err := discord.GetOAuthLink(cfg.Discord, state, cfg.HWSAuth.TrustedHost)
|
||||
link, err := discordAPI.GetOAuthLink(state)
|
||||
if err != nil {
|
||||
throwInternalServiceError(server, w, r, "An error occurred trying to generate the login link", err)
|
||||
return
|
||||
}
|
||||
|
||||
// SUCCESS POINT: OAuth link generated, redirecting to Discord
|
||||
// Clear redirect tracking - user successfully initiated OAuth
|
||||
st.ClearRedirectTrack(r, "/login")
|
||||
|
||||
http.Redirect(w, r, link, http.StatusSeeOther)
|
||||
|
||||
59
internal/handlers/logout.go
Normal file
59
internal/handlers/logout.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.haelnorr.com/h/golib/hws"
|
||||
"git.haelnorr.com/h/golib/hwsauth"
|
||||
"git.haelnorr.com/h/oslstats/internal/db"
|
||||
"git.haelnorr.com/h/oslstats/internal/discord"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func Logout(
|
||||
server *hws.Server,
|
||||
auth *hwsauth.Authenticator[*db.User, bun.Tx],
|
||||
conn *bun.DB,
|
||||
discordAPI *discord.APIClient,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
throwInternalServiceError(server, w, r, "Database error", errors.Wrap(err, "conn.BeginTx"))
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
user := db.CurrentUser(r.Context())
|
||||
if user == nil {
|
||||
// JIC - should be impossible to get here if route is protected by LoginReq
|
||||
w.Header().Set("HX-Redirect", "/")
|
||||
return
|
||||
}
|
||||
token, err := user.DeleteDiscordTokens(ctx, tx)
|
||||
if err != nil {
|
||||
throwInternalServiceError(server, w, r, "Database error", errors.Wrap(err, "user.DeleteDiscordTokens"))
|
||||
return
|
||||
}
|
||||
err = discordAPI.RevokeToken(token.Convert())
|
||||
if err != nil {
|
||||
throwInternalServiceError(server, w, r, "Discord API error", errors.Wrap(err, "discordAPI.RevokeToken"))
|
||||
return
|
||||
}
|
||||
err = auth.Logout(tx, w, r)
|
||||
if err != nil {
|
||||
throwInternalServiceError(server, w, r, "Logout failed", err)
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
w.Header().Set("HX-Redirect", "/")
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -102,38 +102,6 @@ func Register(
|
||||
)
|
||||
}
|
||||
|
||||
func IsUsernameUnique(
|
||||
server *hws.Server,
|
||||
conn *bun.DB,
|
||||
cfg *config.Config,
|
||||
store *store.Store,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.FormValue("username")
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
throwInternalServiceError(server, w, r, "Database transaction failed", err)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
unique, err := db.IsUsernameUnique(ctx, tx, username)
|
||||
if err != nil {
|
||||
throwInternalServiceError(server, w, r, "Database query failed", err)
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
if !unique {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func registerUser(
|
||||
ctx context.Context,
|
||||
tx bun.Tx,
|
||||
@@ -151,7 +119,7 @@ func registerUser(
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db.CreateUser")
|
||||
}
|
||||
err = db.UpdateDiscordToken(ctx, tx, user, details.Token)
|
||||
err = user.UpdateDiscordToken(ctx, tx, details.Token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db.UpdateDiscordToken")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user