2024-02-11 19:28:00 +00:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-02-12 08:25:11 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"encoding/hex"
|
2024-02-11 22:48:37 +00:00
|
|
|
"encoding/json"
|
2024-02-20 10:24:04 +00:00
|
|
|
"errors"
|
2024-02-12 08:25:11 +00:00
|
|
|
"fmt"
|
2024-02-11 19:28:00 +00:00
|
|
|
"io"
|
|
|
|
"net/http"
|
2024-02-16 14:07:52 +00:00
|
|
|
"strconv"
|
2024-02-12 08:25:11 +00:00
|
|
|
"time"
|
2024-02-11 19:28:00 +00:00
|
|
|
|
2024-02-20 10:24:04 +00:00
|
|
|
"github.com/labstack/echo/v4"
|
2024-05-23 16:33:30 +00:00
|
|
|
"github.com/mentos1386/zdravko/database/models"
|
|
|
|
"github.com/mentos1386/zdravko/internal/config"
|
|
|
|
"github.com/mentos1386/zdravko/internal/server/services"
|
2024-02-11 19:28:00 +00:00
|
|
|
"golang.org/x/oauth2"
|
|
|
|
)
|
|
|
|
|
2024-05-25 10:59:13 +00:00
|
|
|
const oauth2RedirectSessionName = "zdravko-hey-oauth2"
|
|
|
|
|
|
|
|
func (h *BaseHandler) setOAuth2Redirect(c echo.Context, redirect string) error {
|
|
|
|
w := c.Response()
|
|
|
|
r := c.Request()
|
|
|
|
|
|
|
|
session, err := h.store.Get(r, oauth2RedirectSessionName)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
session.Values["redirect"] = redirect
|
|
|
|
return h.store.Save(r, w, session)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *BaseHandler) getOAuth2Redirect(c echo.Context) (string, error) {
|
|
|
|
r := c.Request()
|
|
|
|
|
|
|
|
session, err := h.store.Get(r, oauth2RedirectSessionName)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
if session.IsNew {
|
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
return session.Values["redirect"].(string), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *BaseHandler) clearOAuth2Redirect(c echo.Context) error {
|
|
|
|
w := c.Response()
|
|
|
|
r := c.Request()
|
|
|
|
|
|
|
|
session, err := h.store.Get(r, oauth2RedirectSessionName)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
session.Options.MaxAge = -1
|
|
|
|
return h.store.Save(r, w, session)
|
|
|
|
}
|
|
|
|
|
2024-02-11 22:48:37 +00:00
|
|
|
type UserInfo struct {
|
2024-02-16 14:07:52 +00:00
|
|
|
Id int `json:"id"` // FIXME: This might not always be int?
|
2024-02-11 22:48:37 +00:00
|
|
|
Sub string `json:"sub"`
|
|
|
|
Email string `json:"email"`
|
|
|
|
}
|
|
|
|
|
2024-02-12 08:25:11 +00:00
|
|
|
func newRandomState() string {
|
|
|
|
b := make([]byte, 16)
|
|
|
|
_, err := rand.Read(b)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return hex.EncodeToString(b)
|
|
|
|
}
|
|
|
|
|
2024-02-19 09:09:30 +00:00
|
|
|
func newOAuth2(config *config.ServerConfig) *oauth2.Config {
|
2024-02-11 19:28:00 +00:00
|
|
|
return &oauth2.Config{
|
2024-02-15 17:43:35 +00:00
|
|
|
ClientID: config.OAuth2.ClientID,
|
|
|
|
ClientSecret: config.OAuth2.ClientSecret,
|
|
|
|
Scopes: config.OAuth2.Scopes,
|
|
|
|
RedirectURL: config.RootUrl + "/oauth2/callback",
|
2024-02-11 19:28:00 +00:00
|
|
|
Endpoint: oauth2.Endpoint{
|
2024-02-15 17:43:35 +00:00
|
|
|
TokenURL: config.OAuth2.EndpointTokenURL,
|
|
|
|
AuthURL: config.OAuth2.EndpointAuthURL,
|
2024-02-11 19:28:00 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-11 22:48:37 +00:00
|
|
|
func (h *BaseHandler) AuthenticatedUserToOAuth2Token(user *AuthenticatedUser) *oauth2.Token {
|
|
|
|
return &oauth2.Token{
|
|
|
|
AccessToken: user.OAuth2AccessToken,
|
|
|
|
TokenType: user.OAuth2TokenType,
|
|
|
|
RefreshToken: user.OAuth2RefreshToken,
|
|
|
|
Expiry: user.OAuth2Expiry,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *BaseHandler) RefreshToken(w http.ResponseWriter, r *http.Request, user *AuthenticatedUser) (*AuthenticatedUser, error) {
|
|
|
|
tok := h.AuthenticatedUserToOAuth2Token(user)
|
|
|
|
conf := newOAuth2(h.config)
|
|
|
|
refreshed, err := conf.TokenSource(context.Background(), tok).Token()
|
|
|
|
if err != nil {
|
2024-02-12 08:25:11 +00:00
|
|
|
fmt.Println("Error: ", err)
|
2024-02-11 22:48:37 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
refreshedUser := &AuthenticatedUser{
|
|
|
|
ID: user.ID,
|
|
|
|
Email: user.Email,
|
|
|
|
OAuth2AccessToken: refreshed.AccessToken,
|
|
|
|
OAuth2RefreshToken: refreshed.RefreshToken,
|
|
|
|
OAuth2TokenType: refreshed.TokenType,
|
|
|
|
OAuth2Expiry: refreshed.Expiry,
|
|
|
|
}
|
|
|
|
|
|
|
|
err = h.SetAuthenticatedUserForRequest(w, r, refreshedUser)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return refreshedUser, nil
|
|
|
|
}
|
|
|
|
|
2024-02-20 10:24:04 +00:00
|
|
|
func (h *BaseHandler) OAuth2LoginGET(c echo.Context) error {
|
2024-02-23 21:08:17 +00:00
|
|
|
ctx := context.Background()
|
2024-02-11 19:28:00 +00:00
|
|
|
conf := newOAuth2(h.config)
|
|
|
|
|
2024-02-12 08:25:11 +00:00
|
|
|
state := newRandomState()
|
2024-02-29 22:42:56 +00:00
|
|
|
err := services.CreateOAuth2State(ctx, h.db, &models.OAuth2State{
|
|
|
|
State: state,
|
2024-03-01 09:53:36 +00:00
|
|
|
ExpiresAt: &models.Time{Time: time.Now().Add(5 * time.Minute)},
|
2024-02-29 22:42:56 +00:00
|
|
|
})
|
2024-02-23 21:08:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2024-02-12 08:25:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
url := conf.AuthCodeURL(state, oauth2.AccessTypeOffline)
|
2024-02-11 19:28:00 +00:00
|
|
|
|
2024-05-25 10:59:13 +00:00
|
|
|
redirect := c.QueryParam("redirect")
|
|
|
|
h.logger.Info("OAuth2LoginGET", "redirect", redirect)
|
|
|
|
|
|
|
|
err = h.setOAuth2Redirect(c, redirect)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-02-20 10:24:04 +00:00
|
|
|
return c.Redirect(http.StatusTemporaryRedirect, url)
|
2024-02-11 19:28:00 +00:00
|
|
|
}
|
|
|
|
|
2024-02-20 10:24:04 +00:00
|
|
|
func (h *BaseHandler) OAuth2CallbackGET(c echo.Context) error {
|
2024-02-11 19:28:00 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
conf := newOAuth2(h.config)
|
|
|
|
|
2024-02-20 10:24:04 +00:00
|
|
|
state := c.QueryParam("state")
|
|
|
|
code := c.QueryParam("code")
|
2024-02-12 08:25:11 +00:00
|
|
|
|
2024-02-27 11:04:05 +00:00
|
|
|
deleted, err := services.DeleteOAuth2State(ctx, h.db, state)
|
2024-02-12 08:25:11 +00:00
|
|
|
if err != nil {
|
2024-02-20 10:24:04 +00:00
|
|
|
return err
|
2024-02-12 08:25:11 +00:00
|
|
|
}
|
2024-02-29 22:42:56 +00:00
|
|
|
if !deleted {
|
2024-02-20 10:24:04 +00:00
|
|
|
return errors.New("invalid state")
|
2024-02-12 08:25:11 +00:00
|
|
|
}
|
|
|
|
|
2024-02-11 19:28:00 +00:00
|
|
|
// Exchange the code for a new token.
|
2024-02-20 10:24:04 +00:00
|
|
|
tok, err := conf.Exchange(ctx, code)
|
2024-02-11 19:28:00 +00:00
|
|
|
if err != nil {
|
2024-02-20 10:24:04 +00:00
|
|
|
return err
|
2024-02-11 19:28:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ge the user information.
|
|
|
|
client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(tok))
|
2024-02-15 17:43:35 +00:00
|
|
|
resp, err := client.Get(h.config.OAuth2.EndpointUserInfoURL)
|
2024-02-11 19:28:00 +00:00
|
|
|
if err != nil {
|
2024-02-20 10:24:04 +00:00
|
|
|
return err
|
2024-02-11 19:28:00 +00:00
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
2024-02-20 10:24:04 +00:00
|
|
|
return err
|
2024-02-11 19:28:00 +00:00
|
|
|
}
|
|
|
|
|
2024-02-11 22:48:37 +00:00
|
|
|
var userInfo UserInfo
|
|
|
|
err = json.Unmarshal(body, &userInfo)
|
2024-02-11 19:28:00 +00:00
|
|
|
if err != nil {
|
2024-02-20 10:24:04 +00:00
|
|
|
return err
|
2024-02-11 19:28:00 +00:00
|
|
|
}
|
|
|
|
|
2024-02-16 14:07:52 +00:00
|
|
|
userId := userInfo.Sub
|
|
|
|
if userInfo.Id != 0 {
|
|
|
|
userId = strconv.Itoa(userInfo.Id)
|
2024-02-16 12:52:27 +00:00
|
|
|
}
|
|
|
|
|
2024-02-20 10:24:04 +00:00
|
|
|
err = h.SetAuthenticatedUserForRequest(c.Response(), c.Request(), &AuthenticatedUser{
|
2024-02-16 12:52:27 +00:00
|
|
|
ID: userId,
|
2024-02-11 22:48:37 +00:00
|
|
|
Email: userInfo.Email,
|
|
|
|
OAuth2AccessToken: tok.AccessToken,
|
|
|
|
OAuth2RefreshToken: tok.RefreshToken,
|
|
|
|
OAuth2TokenType: tok.TokenType,
|
|
|
|
OAuth2Expiry: tok.Expiry,
|
|
|
|
})
|
|
|
|
if err != nil {
|
2024-02-20 10:24:04 +00:00
|
|
|
return err
|
2024-02-11 22:48:37 +00:00
|
|
|
}
|
|
|
|
|
2024-05-25 10:59:13 +00:00
|
|
|
redirect, err := h.getOAuth2Redirect(c)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
h.logger.Info("OAuth2CallbackGET", "redirect", redirect)
|
|
|
|
if redirect == "" {
|
|
|
|
redirect = "/settings"
|
|
|
|
}
|
|
|
|
|
|
|
|
err = h.clearOAuth2Redirect(c)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.Redirect(http.StatusTemporaryRedirect, redirect)
|
2024-02-11 22:48:37 +00:00
|
|
|
}
|
|
|
|
|
2024-02-20 10:24:04 +00:00
|
|
|
func (h *BaseHandler) OAuth2LogoutGET(c echo.Context) error {
|
|
|
|
cc := c.(AuthenticatedContext)
|
|
|
|
|
2024-02-16 12:52:27 +00:00
|
|
|
if h.config.OAuth2.EndpointLogoutURL != "" {
|
2024-02-20 10:24:04 +00:00
|
|
|
tok := h.AuthenticatedUserToOAuth2Token(cc.Principal.User)
|
2024-02-16 12:52:27 +00:00
|
|
|
client := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(tok))
|
|
|
|
_, err := client.Get(h.config.OAuth2.EndpointLogoutURL)
|
|
|
|
if err != nil {
|
2024-02-20 10:24:04 +00:00
|
|
|
return err
|
2024-02-16 12:52:27 +00:00
|
|
|
}
|
2024-02-11 22:48:37 +00:00
|
|
|
}
|
|
|
|
|
2024-02-20 10:24:04 +00:00
|
|
|
err := h.ClearAuthenticatedUserForRequest(c.Response(), c.Request())
|
2024-02-11 22:48:37 +00:00
|
|
|
if err != nil {
|
2024-02-20 10:24:04 +00:00
|
|
|
return err
|
2024-02-11 22:48:37 +00:00
|
|
|
}
|
2024-02-20 10:24:04 +00:00
|
|
|
|
|
|
|
return c.Redirect(http.StatusTemporaryRedirect, "/")
|
2024-02-11 19:28:00 +00:00
|
|
|
}
|