163 lines
5.0 KiB
Go
163 lines
5.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/labstack/echo/v5"
|
|
"trankilou.fr/lassistanoque/backend/internal/domain"
|
|
"trankilou.fr/lassistanoque/backend/internal/service/auth"
|
|
)
|
|
|
|
func NewAuthGroup(prefix string, e *echo.Group, service *auth.Service) *echo.Group {
|
|
authHandler := &AuthHandler{
|
|
authService: service,
|
|
}
|
|
auth := e.Group(prefix)
|
|
auth.POST("/login", authHandler.Login)
|
|
auth.GET("/oidc/providers", authHandler.OIDCProviders)
|
|
auth.GET("/oidc/login", authHandler.OIDCLogin)
|
|
auth.GET("/oidc/callback", authHandler.OIDCCallback)
|
|
auth.POST("/register", authHandler.Register)
|
|
return auth
|
|
}
|
|
|
|
type AuthHandler struct {
|
|
authService *auth.Service
|
|
}
|
|
|
|
type loginRequest struct {
|
|
Email string `json:"email" validate:"required,email"`
|
|
Password string `json:"password" validate:"required"`
|
|
}
|
|
|
|
type loginResponse struct {
|
|
AccessToken string `json:"accessToken"`
|
|
RefreshToken string `json:"refreshToken"`
|
|
ExpiresAt string `json:"expiresAt"`
|
|
User *domain.User `json:"user"`
|
|
}
|
|
|
|
func (h AuthHandler) Login(c *echo.Context) error {
|
|
var req loginRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
|
|
}
|
|
if req.Email == "" || req.Password == "" {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "email and password are required")
|
|
}
|
|
|
|
creds := auth.Credentials{
|
|
Method: "password",
|
|
Email: req.Email,
|
|
Password: req.Password,
|
|
}
|
|
|
|
session, err := h.authService.Login(context.Background(), creds)
|
|
if err != nil {
|
|
c.Logger().Error(fmt.Sprintf("error while login: %s", err))
|
|
return echo.NewHTTPError(http.StatusUnauthorized, "login error")
|
|
}
|
|
|
|
resp := loginResponse{
|
|
AccessToken: session.AccessToken,
|
|
RefreshToken: session.RefreshToken,
|
|
ExpiresAt: session.ExpiresAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
}
|
|
resp.User = session.User
|
|
|
|
return c.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
// oidcRedirectURI reconstruit l'URL de callback du serveur à partir de la
|
|
// requête. Elle doit être identique entre /oidc/login et /oidc/callback.
|
|
func oidcRedirectURI(c *echo.Context) string {
|
|
scheme := "http"
|
|
if proto := c.Request().Header.Get("X-Forwarded-Proto"); proto != "" {
|
|
scheme = proto
|
|
} else if c.Request().TLS != nil {
|
|
scheme = "https"
|
|
}
|
|
host := c.Request().Header.Get("X-Forwarded-Host")
|
|
if host == "" {
|
|
host = c.Request().Host
|
|
}
|
|
return scheme + "://" + host + "/api/auth/oidc/callback"
|
|
}
|
|
|
|
func (h AuthHandler) OIDCProviders(c *echo.Context) error {
|
|
providers, err := h.authService.ListOIDCProviders()
|
|
if err != nil {
|
|
c.Logger().Error(fmt.Sprintf("error listing oidc providers: %s", err))
|
|
return echo.NewHTTPError(http.StatusInternalServerError, "error listing oidc providers")
|
|
}
|
|
return c.JSON(http.StatusOK, providers)
|
|
}
|
|
|
|
func (h AuthHandler) OIDCLogin(c *echo.Context) error {
|
|
providerID := c.QueryParam("provider")
|
|
if providerID == "" {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "provider query parameter is required")
|
|
}
|
|
|
|
authorizeURL, err := h.authService.OIDCAuthorizeURL(context.Background(), providerID, oidcRedirectURI(c))
|
|
if err != nil {
|
|
c.Logger().Error(fmt.Sprintf("error building oidc authorize url: %s", err))
|
|
return c.Redirect(http.StatusFound, "/login?error=oidc")
|
|
}
|
|
return c.Redirect(http.StatusFound, authorizeURL)
|
|
}
|
|
|
|
func (h AuthHandler) OIDCCallback(c *echo.Context) error {
|
|
if errParam := c.QueryParam("error"); errParam != "" {
|
|
c.Logger().Error(fmt.Sprintf("oidc provider returned an error: %s", errParam))
|
|
return c.Redirect(http.StatusFound, "/login?error=oidc")
|
|
}
|
|
code := c.QueryParam("code")
|
|
state := c.QueryParam("state")
|
|
if code == "" || state == "" {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "code and state are required")
|
|
}
|
|
|
|
session, err := h.authService.OIDCCallback(context.Background(), state, code, oidcRedirectURI(c))
|
|
if err != nil {
|
|
c.Logger().Error(fmt.Sprintf("error while oidc callback: %s", err))
|
|
return c.Redirect(http.StatusFound, "/login?error=oidc")
|
|
}
|
|
|
|
// L'application SPA est servie par ce même serveur (même origine) et lit
|
|
// ses tokens dans localStorage: on les dépose ici puis on redirige vers
|
|
// l'application, sans jamais les faire transiter dans une URL.
|
|
page := fmt.Sprintf(`<!doctype html>
|
|
<html>
|
|
<head><meta charset="utf-8"><title>Connexion en cours...</title></head>
|
|
<body>
|
|
<script>
|
|
localStorage.setItem("accessToken", %q);
|
|
localStorage.setItem("refreshToken", %q);
|
|
location.replace("/");
|
|
</script>
|
|
</body>
|
|
</html>`, session.AccessToken, session.RefreshToken)
|
|
return c.Blob(http.StatusOK, "text/html; charset=utf-8", []byte(page))
|
|
}
|
|
|
|
func (h AuthHandler) Register(c *echo.Context) error {
|
|
|
|
reader := c.Request().Body
|
|
var registration auth.Registration
|
|
decoder := json.NewDecoder(reader)
|
|
err := decoder.Decode(®istration)
|
|
if err != nil {
|
|
return c.String(http.StatusBadRequest, "malformatted registration")
|
|
}
|
|
|
|
user, err := h.authService.Register(registration)
|
|
if err != nil {
|
|
return c.String(http.StatusBadRequest, err.Error())
|
|
}
|
|
return c.JSON(http.StatusCreated, user)
|
|
}
|