153 lines
3.5 KiB
Go
153 lines
3.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"git.elisiei.xyz/elisiei/dbots/internal/config"
|
|
"git.elisiei.xyz/elisiei/dbots/internal/db"
|
|
"git.elisiei.xyz/elisiei/dbots/internal/discord"
|
|
"git.elisiei.xyz/elisiei/dbots/internal/errorutil"
|
|
"git.elisiei.xyz/elisiei/dbots/internal/middleware"
|
|
"git.elisiei.xyz/elisiei/dbots/internal/token"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/render"
|
|
)
|
|
|
|
const cookieName = "session"
|
|
|
|
type Router struct {
|
|
auth *Service
|
|
router chi.Router
|
|
config config.Config
|
|
queries *db.Queries
|
|
}
|
|
|
|
func NewRouter(q *db.Queries, client *discord.Client, config config.Config) *Router {
|
|
return &Router{
|
|
auth: NewService(q, client),
|
|
router: chi.NewRouter(),
|
|
config: config,
|
|
queries: q,
|
|
}
|
|
}
|
|
|
|
func (r *Router) Routes() http.Handler {
|
|
r.router.Get("/login", r.login)
|
|
r.router.Get("/callback", r.callback)
|
|
r.router.Post("/logout", r.logout)
|
|
r.router.With(middleware.AuthGuardMiddleware).Get("/me", r.me)
|
|
return r.router
|
|
}
|
|
|
|
func (r *Router) me(w http.ResponseWriter, req *http.Request) {
|
|
user := middleware.GetUser(req.Context())
|
|
if user == nil {
|
|
render.Render(w, req, errorutil.ErrUnauthorized)
|
|
return
|
|
}
|
|
render.JSON(w, req, user)
|
|
}
|
|
|
|
func (r *Router) login(w http.ResponseWriter, req *http.Request) {
|
|
state, err := GenerateState()
|
|
if err != nil {
|
|
render.Render(w, req, errorutil.ErrInternal(err))
|
|
return
|
|
}
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "oauth_state",
|
|
Value: state,
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: 300,
|
|
Path: "/",
|
|
})
|
|
|
|
http.Redirect(w, req, r.auth.client.AuthURL(state), http.StatusFound)
|
|
}
|
|
|
|
func (r *Router) callback(w http.ResponseWriter, req *http.Request) {
|
|
stateCookie, err := req.Cookie("oauth_state")
|
|
if err != nil || stateCookie.Value != req.URL.Query().Get("state") {
|
|
render.Render(w, req, errorutil.ErrUnauthorized)
|
|
return
|
|
}
|
|
http.SetCookie(w, &http.Cookie{Name: "oauth_state", MaxAge: -1, Path: "/"})
|
|
|
|
code := req.URL.Query().Get("code")
|
|
if code == "" {
|
|
render.Render(w, req, errorutil.ErrInvalidRequest(nil))
|
|
return
|
|
}
|
|
|
|
user, _, err := r.auth.Callback(req.Context(), code)
|
|
if err != nil {
|
|
render.Render(w, req, errorutil.ErrInternal(err))
|
|
return
|
|
}
|
|
|
|
jti, err := GenerateState()
|
|
if err != nil {
|
|
render.Render(w, req, errorutil.ErrInternal(err))
|
|
return
|
|
}
|
|
|
|
_, err = r.queries.CreateSession(req.Context(), db.CreateSessionParams{
|
|
ID: jti,
|
|
UserID: user.ID,
|
|
ExpiresAt: time.Now().Add(token.TokenDuration),
|
|
})
|
|
if err != nil {
|
|
render.Render(w, req, errorutil.ErrInternal(err))
|
|
return
|
|
}
|
|
|
|
key, err := token.KeyFromHex(r.config.Auth.PasetoKey)
|
|
if err != nil {
|
|
render.Render(w, req, errorutil.ErrInternal(err))
|
|
return
|
|
}
|
|
|
|
raw, err := token.IssueToken(key, user.ID, jti)
|
|
if err != nil {
|
|
render.Render(w, req, errorutil.ErrInternal(err))
|
|
return
|
|
}
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: cookieName,
|
|
Value: raw,
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteStrictMode,
|
|
MaxAge: int(token.TokenDuration.Seconds()),
|
|
Path: "/",
|
|
Secure: r.config.Production,
|
|
})
|
|
|
|
http.Redirect(w, req, "/", http.StatusFound)
|
|
}
|
|
|
|
func (r *Router) logout(w http.ResponseWriter, req *http.Request) {
|
|
c, err := req.Cookie(cookieName)
|
|
if err != nil {
|
|
render.NoContent(w, req)
|
|
return
|
|
}
|
|
|
|
key, err := token.KeyFromHex(r.config.Auth.PasetoKey)
|
|
if err == nil {
|
|
if claims, err := token.VerifyToken(key, c.Value); err == nil {
|
|
_ = r.queries.RevokeSession(req.Context(), claims.JTI)
|
|
}
|
|
}
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: cookieName,
|
|
MaxAge: -1,
|
|
Path: "/",
|
|
})
|
|
render.NoContent(w, req)
|
|
}
|