205 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			205 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package server
 | 
						|
 | 
						|
import (
 | 
						|
	"crypto/ecdsa"
 | 
						|
	"crypto/elliptic"
 | 
						|
	"crypto/rand"
 | 
						|
	"crypto/x509"
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"log/slog"
 | 
						|
	"net/http"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"git.t-juice.club/microfilm/auth"
 | 
						|
	"git.t-juice.club/microfilm/auth/store"
 | 
						|
	"github.com/go-chi/chi/v5"
 | 
						|
	"github.com/go-chi/chi/v5/middleware"
 | 
						|
	"github.com/golang-jwt/jwt/v5"
 | 
						|
	"github.com/google/uuid"
 | 
						|
	"github.com/nats-io/nats.go"
 | 
						|
)
 | 
						|
 | 
						|
const DefaultTokenDuration time.Duration = 24 * time.Hour
 | 
						|
 | 
						|
type Server struct {
 | 
						|
	Logger *slog.Logger
 | 
						|
 | 
						|
	http.Server
 | 
						|
	store      store.AuthStore
 | 
						|
	config     *Config
 | 
						|
	nats       *nats.EncodedConn
 | 
						|
	userClient *UserClient
 | 
						|
 | 
						|
	signingKey *ecdsa.PrivateKey
 | 
						|
}
 | 
						|
 | 
						|
func NewServer(config *Config) (*Server, error) {
 | 
						|
	srv := &Server{}
 | 
						|
 | 
						|
	r := chi.NewRouter()
 | 
						|
 | 
						|
	r.Use(middleware.RequestID)
 | 
						|
	r.Use(srv.MiddlewareLogging)
 | 
						|
 | 
						|
	r.Get("/key", srv.PubkeyHandler)
 | 
						|
	r.Post("/{id}/token", srv.TokenHandler)
 | 
						|
 | 
						|
	srv.Handler = r
 | 
						|
	srv.Addr = config.ListenAddr
 | 
						|
 | 
						|
	srv.config = config
 | 
						|
 | 
						|
	srv.Logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{
 | 
						|
		Level: slog.LevelDebug,
 | 
						|
	}))
 | 
						|
 | 
						|
	srv.store = store.NewMemoryAuthStore()
 | 
						|
 | 
						|
	conn, err := nats.Connect(config.NATSAddr)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	encoded, err := nats.NewEncodedConn(conn, "json")
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	srv.nats = encoded
 | 
						|
	srv.userClient = NewUserClient(config.UserServiceBaseURL)
 | 
						|
 | 
						|
	// Generate keys
 | 
						|
	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	srv.signingKey = privateKey
 | 
						|
 | 
						|
	return srv, nil
 | 
						|
}
 | 
						|
 | 
						|
func InfoHandler(w http.ResponseWriter, r *http.Request) {
 | 
						|
	enc := json.NewEncoder(w)
 | 
						|
 | 
						|
	data := &auth.InfoResponse{
 | 
						|
		Version: auth.Version,
 | 
						|
	}
 | 
						|
 | 
						|
	_ = enc.Encode(data)
 | 
						|
}
 | 
						|
 | 
						|
func WriteError(w http.ResponseWriter, response auth.ErrorResponse) {
 | 
						|
	encoder := json.NewEncoder(w)
 | 
						|
	w.WriteHeader(response.Status)
 | 
						|
	_ = encoder.Encode(&response)
 | 
						|
}
 | 
						|
 | 
						|
func (s *Server) PubkeyHandler(w http.ResponseWriter, r *http.Request) {
 | 
						|
	enc := json.NewEncoder(w)
 | 
						|
	key, err := x509.MarshalPKIXPublicKey(s.signingKey.Public())
 | 
						|
	if err != nil {
 | 
						|
		s.Logger.Error("Unable to marshal public key.", "error", err)
 | 
						|
		WriteError(w, auth.ErrorResponse{
 | 
						|
			Status:  http.StatusInternalServerError,
 | 
						|
			Message: "Unable to marshal public key",
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
	response := auth.PubkeyResponse{
 | 
						|
		PubKey: key,
 | 
						|
	}
 | 
						|
 | 
						|
	_ = enc.Encode(&response)
 | 
						|
}
 | 
						|
 | 
						|
func (s *Server) TokenHandler(w http.ResponseWriter, r *http.Request) {
 | 
						|
	decoder := json.NewDecoder(r.Body)
 | 
						|
	defer r.Body.Close()
 | 
						|
 | 
						|
	userIdentifier := chi.URLParam(r, "id")
 | 
						|
	if userIdentifier == "" {
 | 
						|
		WriteError(w, auth.ErrorResponse{
 | 
						|
			Status:  http.StatusBadRequest,
 | 
						|
			Message: "Invalid user identifier.",
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	var request auth.TokenRequest
 | 
						|
	if err := decoder.Decode(&request); err != nil {
 | 
						|
		WriteError(w, auth.ErrorResponse{
 | 
						|
			Status:  http.StatusBadRequest,
 | 
						|
			Message: fmt.Sprintf("Error parsing request: %s", err),
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if err := s.userClient.VerifyUserPassword(userIdentifier, request.Password); err != nil {
 | 
						|
		WriteError(w, auth.ErrorResponse{
 | 
						|
			Status:  http.StatusUnauthorized,
 | 
						|
			Message: fmt.Sprintf("Unable to verify password: %s", err),
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	exp := time.Now().Add(DefaultTokenDuration)
 | 
						|
	claims := auth.MicrofilmClaims{
 | 
						|
		Role: auth.RoleUser,
 | 
						|
		RegisteredClaims: jwt.RegisteredClaims{
 | 
						|
			Issuer:    "microfilm",
 | 
						|
			Subject:   userIdentifier,
 | 
						|
			Audience:  []string{"microfilm"},
 | 
						|
			ExpiresAt: jwt.NewNumericDate(exp),
 | 
						|
			NotBefore: jwt.NewNumericDate(time.Now()),
 | 
						|
			IssuedAt:  jwt.NewNumericDate(time.Now()),
 | 
						|
			ID:        uuid.Must(uuid.NewRandom()).String(),
 | 
						|
		},
 | 
						|
	}
 | 
						|
	token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
 | 
						|
	tokenString, err := token.SignedString(s.signingKey)
 | 
						|
	if err != nil {
 | 
						|
		WriteError(w, auth.ErrorResponse{
 | 
						|
			Status:  http.StatusInternalServerError,
 | 
						|
			Message: fmt.Sprintf("Unable to sign token: %s", err),
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if err := s.store.Add(store.RevokableToken{
 | 
						|
		ID:        claims.RegisteredClaims.ID,
 | 
						|
		Subject:   claims.RegisteredClaims.Subject,
 | 
						|
		ExpiresAt: exp,
 | 
						|
	}); err != nil {
 | 
						|
		WriteError(w, auth.ErrorResponse{
 | 
						|
			Status:  http.StatusInternalServerError,
 | 
						|
			Message: fmt.Sprintf("Unable to store token for revocation: %s", err),
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// Publish message
 | 
						|
	subject := fmt.Sprintf("%s.%s", s.config.BaseSubject, "issued")
 | 
						|
	msg := auth.MsgTokenIssued{
 | 
						|
		Message: "Token issued.",
 | 
						|
		ID:      claims.RegisteredClaims.ID,
 | 
						|
		Subject: claims.RegisteredClaims.Subject,
 | 
						|
	}
 | 
						|
	if err := s.nats.Publish(subject, msg); err != nil {
 | 
						|
		s.Logger.Error("Unable to publish message.", "error", err)
 | 
						|
		WriteError(w, auth.ErrorResponse{
 | 
						|
			Status:  http.StatusInternalServerError,
 | 
						|
			Message: fmt.Sprintf("Unable to store token for revocation: %s", err),
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	response := auth.TokenResponse{
 | 
						|
		Token: tokenString,
 | 
						|
	}
 | 
						|
 | 
						|
	w.WriteHeader(http.StatusOK)
 | 
						|
	encoder := json.NewEncoder(w)
 | 
						|
	_ = encoder.Encode(&response)
 | 
						|
}
 |