2024-04-03 02:43:57 +08:00
|
|
|
package auth
|
2024-02-05 00:32:57 +08:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/rand"
|
2024-05-07 13:38:31 +08:00
|
|
|
"crypto/subtle"
|
2024-02-05 00:32:57 +08:00
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
2024-04-28 01:31:23 +08:00
|
|
|
"sync"
|
2024-02-05 00:32:57 +08:00
|
|
|
|
|
|
|
"github.com/coreos/go-oidc/v3/oidc"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
|
|
"github.com/labstack/echo/v4/middleware"
|
|
|
|
"golang.org/x/oauth2"
|
|
|
|
)
|
|
|
|
|
2024-04-03 02:43:57 +08:00
|
|
|
// 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"`
|
2024-02-05 00:32:57 +08:00
|
|
|
|
|
|
|
// Skipper defines a function to skip middleware.
|
|
|
|
Skipper middleware.Skipper
|
|
|
|
}
|
|
|
|
|
2024-04-03 02:43:57 +08:00
|
|
|
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 {
|
2024-05-07 13:38:31 +08:00
|
|
|
tokens map[string][]byte
|
|
|
|
sync.RWMutex
|
2024-04-28 01:31:23 +08:00
|
|
|
|
2024-04-02 17:00:12 +08:00
|
|
|
cfg oauth2.Config
|
|
|
|
verifier *oidc.IDTokenVerifier
|
|
|
|
skipper middleware.Skipper
|
|
|
|
}
|
|
|
|
|
2024-04-03 02:43:57 +08:00
|
|
|
func New(cfg Config) *Auth {
|
|
|
|
provider, err := oidc.NewProvider(context.Background(), cfg.OIDC.ProviderURL)
|
2024-02-05 00:32:57 +08:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
verifier := provider.Verifier(&oidc.Config{
|
2024-04-03 02:43:57 +08:00
|
|
|
ClientID: cfg.OIDC.ClientID,
|
2024-02-05 00:32:57 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
oidcConfig := oauth2.Config{
|
2024-04-03 02:43:57 +08:00
|
|
|
ClientID: cfg.OIDC.ClientID,
|
|
|
|
ClientSecret: cfg.OIDC.ClientSecret,
|
2024-02-05 00:32:57 +08:00
|
|
|
Endpoint: provider.Endpoint(),
|
2024-04-03 02:43:57 +08:00
|
|
|
RedirectURL: cfg.OIDC.RedirectURL,
|
2024-02-05 00:32:57 +08:00
|
|
|
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
|
|
|
}
|
|
|
|
|
2024-04-03 02:43:57 +08:00
|
|
|
return &Auth{
|
2024-04-02 17:00:12 +08:00
|
|
|
verifier: verifier,
|
|
|
|
cfg: oidcConfig,
|
2024-04-03 02:43:57 +08:00
|
|
|
skipper: cfg.OIDC.Skipper,
|
2024-04-02 17:00:12 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-07 13:38:31 +08:00
|
|
|
// SetTokens caches tokens for authenticating API client calls.
|
|
|
|
func (o *Auth) SetAPITokens(tokens map[string][]byte) {
|
|
|
|
o.Lock()
|
|
|
|
defer o.Unlock()
|
2024-04-28 01:31:23 +08:00
|
|
|
|
2024-05-07 13:38:31 +08:00
|
|
|
o.tokens = make(map[string][]byte, len(tokens))
|
|
|
|
for user, token := range tokens {
|
|
|
|
o.tokens[user] = []byte{}
|
|
|
|
copy(o.tokens[user], token)
|
2024-04-28 01:31:23 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-07 13:38:31 +08:00
|
|
|
// CheckAPIToken validates an API user+token.
|
|
|
|
func (o *Auth) CheckAPIToken(user string, token []byte) bool {
|
|
|
|
o.RLock()
|
|
|
|
t, ok := o.tokens[user]
|
|
|
|
o.RUnlock()
|
|
|
|
|
|
|
|
return ok && subtle.ConstantTimeCompare(t, token) == 1
|
2024-04-28 01:31:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// HandleOIDCCallback is the HTTP handler that handles the post-OIDC provider redirect callback.
|
|
|
|
func (o *Auth) HandleOIDCCallback(c echo.Context) error {
|
2024-04-02 17:00:12 +08:00
|
|
|
tk, err := o.cfg.Exchange(c.Request().Context(), c.Request().URL.Query().Get("code"))
|
2024-02-05 00:32:57 +08:00
|
|
|
if err != nil {
|
2024-04-02 17:00:12 +08:00
|
|
|
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.")
|
|
|
|
}
|
|
|
|
|
2024-04-03 02:43:57 +08:00
|
|
|
// 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))
|
|
|
|
// }
|
2024-04-02 17:00:12 +08:00
|
|
|
|
2024-04-03 02:43:57 +08:00
|
|
|
// nonce, err := c.Cookie("nonce")
|
|
|
|
// if err != nil {
|
|
|
|
// return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("nonce cookie not found: %v", err))
|
|
|
|
// }
|
2024-02-05 00:32:57 +08:00
|
|
|
|
2024-04-03 02:43:57 +08:00
|
|
|
// if idTk.Nonce != nonce.Value {
|
|
|
|
// return echo.NewHTTPError(http.StatusUnauthorized, "nonce did not match")
|
|
|
|
// }
|
2024-02-05 00:32:57 +08:00
|
|
|
|
2024-04-02 17:00:12 +08:00
|
|
|
c.SetCookie(&http.Cookie{
|
|
|
|
Name: "id_token",
|
|
|
|
Value: rawIDTk,
|
|
|
|
Secure: true,
|
2024-04-03 02:43:57 +08:00
|
|
|
SameSite: http.SameSiteLaxMode,
|
2024-04-02 17:00:12 +08:00
|
|
|
Path: "/",
|
|
|
|
})
|
|
|
|
|
2024-04-02 17:20:45 +08:00
|
|
|
return c.Redirect(http.StatusTemporaryRedirect, c.Request().URL.Query().Get("state"))
|
2024-04-02 17:00:12 +08:00
|
|
|
}
|
|
|
|
|
2024-04-03 02:43:57 +08:00
|
|
|
func (o *Auth) Middleware(next echo.HandlerFunc) echo.HandlerFunc {
|
2024-04-02 17:00:12 +08:00
|
|
|
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 {
|
2024-04-02 17:20:45 +08:00
|
|
|
// Verify the token.
|
2024-04-03 02:43:57 +08:00
|
|
|
idTk, err := o.verifier.Verify(c.Request().Context(), rawIDTk.Value)
|
2024-04-02 17:20:45 +08:00
|
|
|
if err == nil {
|
2024-04-03 02:43:57 +08:00
|
|
|
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)
|
|
|
|
|
2024-04-02 17:20:45 +08:00
|
|
|
return next(c)
|
|
|
|
}
|
|
|
|
} else if err != http.ErrNoCookie {
|
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
2024-04-02 17:00:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// If the verification failed, redirect to the provider for auth.
|
|
|
|
nonce, err := randString(16)
|
|
|
|
if err != nil {
|
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
2024-02-05 00:32:57 +08:00
|
|
|
}
|
2024-04-02 17:00:12 +08:00
|
|
|
c.SetCookie(&http.Cookie{
|
|
|
|
Name: "nonce",
|
|
|
|
Value: nonce,
|
|
|
|
Secure: true,
|
2024-04-03 02:43:57 +08:00
|
|
|
SameSite: http.SameSiteLaxMode,
|
2024-04-02 17:00:12 +08:00
|
|
|
Path: "/",
|
|
|
|
})
|
2024-04-02 17:20:45 +08:00
|
|
|
return c.Redirect(http.StatusTemporaryRedirect, o.cfg.AuthCodeURL(c.Request().URL.RequestURI(), oidc.Nonce(nonce)))
|
2024-02-05 00:32:57 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|