Caesar Auth

Learn how to secure your application with Caesar Auth.

Overview

The Caesar Auth package provides authentication and authorization functionalities for your application. It includes support for social logins (Google, Facebook, Github). This guide will walk you through setting up Caesar Auth and implementing common authentication features like sign up, sign in, sign out, and social authentication.

Installation

First, download the Caesar Auth package:

go get -u github.com/caesar-rocks/auth

Setting Up Caesar Auth

Validate Environment Variables

Before setting up Caesar Auth, ensure that the necessary environment variables are validated in the ./config/env.go file:

package config

import (
	"github.com/caesar-rocks/core"
)

// EnvironmentVariables is a struct that holds all the environment variables that need to be validated.
type EnvironmentVariables struct {
	APP_KEY                    string `validate:"required"`

    // Validate variables for your OAuth providers
	GITHUB_OAUTH_KEY           string
	GITHUB_OAUTH_SECRET        string
	GITHUB_OAUTH_CALLBACK_URL  string
	
	// Other environment variables...
}

func ProvideEnvironmentVariables() *EnvironmentVariables {
	return core.ValidateEnvironmentVariables[EnvironmentVariables]()
}

Configuration

Create a configuration file located at ./config/auth.go:

package config

import (
	"citadel/app/repositories"
	"context"
	"time"

	"github.com/caesar-rocks/auth"
)

func ProvideAuth(env *EnvironmentVariables, usersRepository *repositories.UsersRepository) *auth.Auth {
	return auth.NewAuth(&auth.AuthCfg{
		Key:           env.APP_KEY,
		JWTSigningKey: []byte(env.APP_KEY),
		MaxAge:        time.Hour * 24 * 30,
		JWTExpiration: time.Hour * 24 * 30,
		UserProvider: func(ctx context.Context, userID any) (any, error) {
			return usersRepository.FindOneBy(ctx, "id", userID)
		},
		RedirectTo: "/auth/sign_in", // Redirect path for unauthenticated users
		SocialProviders: &map[string]auth.SocialAuthProvider{
			"github": { // GitHub social provider (optional)
				Key:         env.GITHUB_OAUTH_KEY,
				Secret:      env.GITHUB_OAUTH_SECRET,
				CallbackURL: env.GITHUB_OAUTH_CALLBACK_URL,
				Scopes:      []string{"user:email", "read:user"},
			},
		},
	})
}

Register as a Provider

Register the ProvideAuth function as a provider in the ./config/app.go file:

package config

import (
	"myapp/app/repositories"

	"github.com/caesar-rocks/core"
)

func ProvideApp() *core.App {
	app := core.NewApp(&core.AppConfig{
		Addr: ":8080",
	})

	env := ProvideEnvironmentVariables()
	auth := ProvideAuth(env, usersRepo)

	// Register providers
	app.RegisterProviders(
		ProvideEnvironmentVariables,
		ProvideAuth,
		// Other providers...
	)

	// Other invokers...

	return app
}

Using Caesar Auth in Controllers

Sign In Controller

Define a controller to handle the sign in functionality:

package authControllers

import (
	"citadel/app/repositories"
	authPages "citadel/views/pages/auth"

	caesarAuth "github.com/caesar-rocks/auth"
	caesar "github.com/caesar-rocks/core"
)

type SignInController struct {
	auth            *caesarAuth.Auth
	usersRepository *repositories.UsersRepository
}

func (c *SignInController) Show(ctx *caesar.CaesarCtx) error {
	return ctx.Render(authPages.SignInPage())
}

type SignInValidator struct {
	Email    string `form:"email" validate:"required,email"`
	Password string `form:"password" validate:"required,min=8"`
}

func (c *SignInController) Handle(ctx *caesar.CaesarCtx) error {
	data, errors, ok := caesar.Validate[SignInValidator](ctx)
	if !ok {
		return ctx.Render(authPages.SignInForm(
			authPages.SignInInput{Email: data.Email, Password: data.Password},
			errors,
		))
	}

	user, _ := c.usersRepository.FindOneBy(ctx.Context(), "email", data.Email)
	if user == nil || !caesarAuth.CheckPasswordHash(data.Password, user.Password) {
		return ctx.Render(authPages.SignInForm(
			authPages.SignInInput{Email: data.Email, Password: data.Password},
			map[string]string{"Auth": "Invalid credentials."},
		))
	}

	return ctx.Redirect("/applications")
}

Sign Up Controller

Define a controller to handle the sign up functionality:

package authControllers

import (
	"citadel/app/models"
	"citadel/app/repositories"
	"citadel/app/services"
	authPages "citadel/views/pages/auth"

	caesarAuth "github.com/caesar-rocks/auth"
	caesar "github.com/caesar-rocks/core"
	"github.com/caesar-rocks/events"
)

type SignUpController struct {
	auth    *caesarAuth.Auth
	repo    *repositories.UsersRepository
	service *services.UsersService
	emitter *events.EventsEmitter
}

type SignUpValidator struct {
	Email    string `form:"email" validate:"required,email"`
	FullName string `form:"full_name" validate:"required,min=3"`
	Password string `form:"password" validate:"required,min=8"`
}

func (c *SignUpController) Handle(ctx *caesar.CaesarCtx) error {
	data, errors, ok := caesar.Validate[SignUpValidator](ctx)
	if !ok {
		return ctx.Render(authPages.SignUpForm(
			authPages.SignUpInput{Email: data.Email, FullName: data.FullName, Password: data.Password},
			errors,
		))
	}

	user, _ := c.repo.FindOneBy(ctx.Context(), "email", data.Email)
	if user != nil {
		return ctx.Render(authPages.SignUpForm(
			authPages.SignUpInput{Email: data.Email, FullName: data.FullName, Password: data.Password},
			map[string]string{"Email": "Email is already in use."},
		))
	}

	hashedPassword, err := caesarAuth.HashPassword(data.Password)
	if err != nil {
		return caesar.NewError(400)
	}

	user = &models.User{Email: data.Email, FullName: data.FullName, Password: hashedPassword}
	if err := c.service.CreateAndEmitEvent(ctx.Context(), user); err != nil {
		return caesar.NewError(400)
	}

	if err := c.auth.Authenticate(ctx, *user); err != nil {
		return caesar.NewError(400)
	}

	return ctx.Redirect("/applications")
}

Sign Out Controller

Define a controller to handle the sign out functionality:

package authControllers

import (
	"github.com/caesar-rocks/auth"
	caesar "github.com/caesar-rocks/core"
)

type SignOutController struct {
	auth *auth.Auth
}

func (c *SignOutController) Handle(ctx *caesar.CaesarCtx) error {
	if err := c.auth.SignOut(ctx); err != nil {
		return err
	}

	return ctx.Redirect("/auth/sign_in")
}

Github Social Auth Controller

Define a controller to handle Github social authentication:

package authControllers

import (
	"citadel/app/models"
	"citadel/app/repositories"
	"citadel/app/services"

	"github.com/caesar-rocks/auth"
	caesar "github.com/caesar-rocks/core"
)

type GithubController struct {
	auth    *auth.Auth
	repo    *repositories.UsersRepository
	service *services.UsersService
}

func (c *GithubController) Redirect(ctx *caesar.CaesarCtx) error {
	return c.auth.Social.Use("github").Redirect(ctx)
}

func (c *GithubController) Callback(ctx *caesar.CaesarCtx) error {
	oauthUser, err := c.auth.Social.Use("github").Callback(ctx)
	if err != nil {
		return caesar.NewError(400)
	}

	user, err := c.repo.FindOneBy(ctx.Context(), "github_user_id", oauthUser.UserID)
	if err != nil {
		user = &models.User{Email: oauthUser.Email, FullName: oauthUser.Name, GitHubUserID: oauthUser.UserID}
		if err := c.service.CreateAndEmitEvent(ctx.Context(), user); err != nil {
			return caesar.NewError(400)
		}
	}

	if err := c.auth.Authenticate(ctx, *user); err != nil {
		return caesar.NewError(400)
	}

	return ctx.Redirect("/applications")
}

Routes Configuration

Register your authentication and social auth routes:

package config

import (
	"myapp/app/controllers"
	"github.com/caesar-rocks/auth"
	caesar "github.com/caesar-rocks/core"
)

func RegisterRoutes(
	auth *auth.Auth,
	usersRepo *repositories.UsersRepository,
) *caesar.Router {
	signInController := authControllers.NewSignInController(auth, usersRepo)
	signUpController := authControllers.NewSignUpController(auth, usersService, usersRepo, emitter)
	signOutController := authControllers.NewSignOutController(auth)
	githubController := authControllers.NewGithubController(auth, usersRepo, usersService)

	router := caesar.NewRouter()

	router.Use(auth.SilentMiddleware) // Optionally protects routes without redirecting unauthenticated users

	// Auth routes
	router.Get("/auth/sign_up", signUpController.Show)
	router.Post("/auth/sign_up", signUpController.Handle)
	router.Get("/auth/sign_in", signInController.Show)
	router.Post("/auth/sign_in", signInController.Handle)
	router.Post("/auth/sign_out", signOutController.Handle).Use(auth.AuthMiddleware) // Protects the route by requiring authentication

	// OAuth-related routes
	router.Get("/auth/github/redirect", githubController.Redirect)
	router.Get("/auth/github/callback", githubController.Callback)

	// Protected routes example
	router.Patch("/apps/{slug}", appsController.Update).Use(auth.AuthMiddleware) // Protects the route by requiring authentication

	return router
}

The SilentMiddleware checks if a user is authenticated and stores this information without redirecting unauthenticated users.

The AuthMiddleware ensures that the user is authenticated before accessing the protected route, redirecting them to the sign-in page if necessary.

Retrieving the User from Context

You can retrieve the authenticated user from the context in your controller using the following method:

package controllers

import (
	"citadel/app/models"
	"github.com/caesar-rocks/auth"
	caesar "github.com/caesar-rocks/core"
)

func (c *SomeController) SomeProtectedAction(ctx *caesar.CaesarCtx) error {
	user, err := auth.RetrieveUserFromCtx[models.User](ctx)
	if err != nil {
		return caesar.NewError(401, "Unauthorized")
	}

	// Use the user object for further processing
	// ...

	return ctx.Render(somePage)
}

Summary

The Caesar Auth package provides robust authentication and authorization features for your application. By following this guide, you can set up and use Caesar Auth to handle sign-up, sign-in, sign-out, and social authentication functionalities efficiently.