listmonk/internal/auth/auth.go
2024-10-13 16:59:51 +05:30

186 lines
4.7 KiB
Go

package auth
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"net/http"
"sync"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"golang.org/x/oauth2"
)
// UserKey is the key on which the User profile is set on echo handlers.
const UserKey = "auth_user"
// User struct holds the email and name of the authenticatd user.
// It's attached to the echo handler.
type User struct {
Email string `json:"name"`
Name string `json:"email"`
Picture string `json:"picture"`
}
type OIDCConfig struct {
Enabled bool `json:"enabled"`
ProviderURL string `json:"provider_url"`
RedirectURL string `json:"redirect_url"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
// Skipper defines a function to skip middleware.
Skipper middleware.Skipper
}
type BasicAuthConfig struct {
Enabled bool `json:"enabled"`
Username string `json:"username"`
Password string `json:"password"`
}
type Config struct {
OIDC OIDCConfig
BasicAuth BasicAuthConfig
}
type Auth struct {
tokens map[string]struct{}
mut sync.RWMutex
cfg oauth2.Config
verifier *oidc.IDTokenVerifier
skipper middleware.Skipper
}
func New(cfg Config) *Auth {
provider, err := oidc.NewProvider(context.Background(), cfg.OIDC.ProviderURL)
if err != nil {
panic(err)
}
verifier := provider.Verifier(&oidc.Config{
ClientID: cfg.OIDC.ClientID,
})
oidcConfig := oauth2.Config{
ClientID: cfg.OIDC.ClientID,
ClientSecret: cfg.OIDC.ClientSecret,
Endpoint: provider.Endpoint(),
RedirectURL: cfg.OIDC.RedirectURL,
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
return &Auth{
verifier: verifier,
cfg: oidcConfig,
skipper: cfg.OIDC.Skipper,
}
}
// SetTokens remembers a list of string API tokens that are used for authenticating
// API queries.
func (o *Auth) SetTokens(tokens []string) {
o.mut.Lock()
defer o.mut.Unlock()
o.tokens = make(map[string]struct{}, len(tokens))
for _, t := range tokens {
o.tokens[t] = struct{}{}
}
}
// CheckToken validates an API token.
func (o *Auth) CheckToken(token string) bool {
_, ok := o.tokens[token]
return ok
}
// HandleOIDCCallback is the HTTP handler that handles the post-OIDC provider redirect callback.
func (o *Auth) HandleOIDCCallback(c echo.Context) error {
tk, err := o.cfg.Exchange(c.Request().Context(), c.Request().URL.Query().Get("code"))
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("error exchanging token: %v", err))
}
rawIDTk, ok := tk.Extra("id_token").(string)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "`id_token` missing.")
}
// idTk, err := o.verifier.Verify(c.Request().Context(), rawIDTk)
// if err != nil {
// return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("error verifying ID token: %v", err))
// }
// nonce, err := c.Cookie("nonce")
// if err != nil {
// return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("nonce cookie not found: %v", err))
// }
// if idTk.Nonce != nonce.Value {
// return echo.NewHTTPError(http.StatusUnauthorized, "nonce did not match")
// }
c.SetCookie(&http.Cookie{
Name: "id_token",
Value: rawIDTk,
Secure: true,
SameSite: http.SameSiteLaxMode,
Path: "/",
})
return c.Redirect(http.StatusTemporaryRedirect, c.Request().URL.Query().Get("state"))
}
func (o *Auth) Middleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if o.skipper != nil && o.skipper(c) {
return next(c)
}
rawIDTk, err := c.Cookie("id_token")
if err == nil {
// Verify the token.
idTk, err := o.verifier.Verify(c.Request().Context(), rawIDTk.Value)
if err == nil {
var user User
if err := idTk.Claims(&user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError,
fmt.Sprintf("error verifying OIDC claim: %v", user))
}
fmt.Println(user)
c.Set(UserKey, user)
return next(c)
}
} else if err != http.ErrNoCookie {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
// If the verification failed, redirect to the provider for auth.
nonce, err := randString(16)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
c.SetCookie(&http.Cookie{
Name: "nonce",
Value: nonce,
Secure: true,
SameSite: http.SameSiteLaxMode,
Path: "/",
})
return c.Redirect(http.StatusTemporaryRedirect, o.cfg.AuthCodeURL(c.Request().URL.RequestURI(), oidc.Nonce(nonce)))
}
}
func randString(nByte int) (string, error) {
b := make([]byte, nByte)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}