authpher

package module
v0.1.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 30, 2025 License: MIT Imports: 5 Imported by: 3

README

authpher

What is that?

Authpher performs users identification, authentication, and authorization acting as middleware, using your actual implementation behind the scenes.

How does it work?

The idea is simple. You implement 3 interfaces:

  • AuthUser - provides user ID and auth hash for certain user
  • AuthnBackend - authenticates user using credentials
  • AuthzBackend - authorizes users by providing its permissions

After that, use Auth middleware for all routes where you want to authenticate users, and use PermissionRequired middleware for all routes you want to protect.

Middlewares for http go package and gin framework are provided as adapters. You also need session manager for authpher to work, so you can use your custom or just go with github.com/39george/authpher/sessions/ginsessions for gin, or github.com/39george/authpher/sessions/httpsessions for go http, both are using scs as a session manager.

Installation

To install, run:

go get github.com/39george/authpher

Also, for example, if you will use ServerMux from http package, install:

go get github.com/39george/authpher/sessions/httpsessions

Or with gin:

go get github.com/39george/authpher/sessions/ginsessions
go get github.com/39george/authpher/adapters/authgin 

Usage

Start with backend implementation:

package authbackend

import (
	"context"
	"errors"

	mapset "github.com/deckarep/golang-set/v2"

	"github.com/39george/authpher"
)

// Example user type
type TestUser struct {
	ID           int32
	Username     string
	PasswordHash string
}

// Implement `AuthUser`

func (tu TestUser) UserId() any {
	return tu.ID

}
func (tu TestUser) SessionAuthHash() []byte {
	return []byte(tu.PasswordHash)
}

// Example credentials type
type Credentials struct {
	Username string
	Password string
}

// Can contain database handlers, app state, etc
type TestBackend struct {}

// Implement `AuthnBackend`

func (mb TestBackend) Authenticate(
	ctx context.Context,
	creds TestCredentials,
) (authpher.AuthUser, error) {
	shouldBe := Credentials{Username: "testuser", Password: "testpassword"}
	if creds == shouldBe {
		return &User{123, "testuser", "testpasswordhash"}, nil
	} else {
		return nil, errors.New("bad credentials")
	}
}

func (mb TestBackend) GetUser(
	ctx context.Context,
	userId any,
) (authpher.AuthUser, error) {
	usrId := userId.(int32)
	if usrId == 123 {
		return &User{123, "testuser", "testpasswordhash"}, nil
	} else {
		return nil, errors.New("bad user id")
	}
}

// Implement `AuthzBackend`

func (mb TestBackend) GetUserPermissions(
	ctx context.Context,
	user authpher.AuthUser,
) (mapset.Set[string], error) {
	perms := mapset.NewSet[string]()
	// Cast to TestUser.ID type
	if user.UserId().(int32) == 123 {
		perms.Add("userpermission")
	}
	return perms, nil
}

func (mb TestBackend) GetGroupPermissions(
	ctx context.Context,
	user authpher.AuthUser,
) (mapset.Set[string], error) {
	perms := mapset.NewSet[string]()
	return perms, nil
}

Then, if you use ServerMux:

package authttp_test

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"testing"
	"time"

	"github.com/39george/authpher"
	"github.com/39george/authpher/adapters/authttp"
	"github.com/39george/authpher/sessions/httpsessions"
	scsRedisStore "github.com/39george/scs_redisstore"
	"github.com/alexedwards/scs/v2"
	"github.com/justinas/alice"
)

func runServer(permission string) (net.Addr, error) {
	// Prepare
	redisClient := helpers.GetRedisConnectionPool()
	err := redisClient.Ping(context.Background()).Err()
	if err != nil {
		return nil, err
	}
	sessionManager := scs.New()
	sessionManager.Store = scsRedisStore.New(redisClient)
	sessionManager.Lifetime = 24 * time.Hour

	auth := authttp.Auth(
		backend.TestBackend{},
		&httpsessions.GoSessions{Store: sessionManager},
	)

	mux := http.NewServeMux()

	// Middleware
	muxChain := alice.New(
		sessionManager.LoadAndSave,
		auth,
	).Then(mux)

	// Open routes
	mux.Handle("/login", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		credentials := new(backend.TestCredentials)
		err := json.NewDecoder(r.Body).Decode(&credentials)
		if err != nil {
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}
		aS := ctx.Value(authpher.AuthContextString)
		authSession := aS.(*authpher.AuthSession[string, testbackend.TestCredentials])
		user, err := authSession.Authenticate(ctx, *credentials)
		if err != nil {
			http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
			return
		}
		if user != nil {
			u := user.(*testbackend.TestUser)
			err = authSession.Login(ctx, u)
			if err != nil {
				http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			} else {
				w.WriteHeader(http.StatusOK)
			}
		} else {
			http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
		}
	}))

	// Protected routes
	protectedMux := http.NewServeMux()
	protectedMux.HandleFunc("/testpath", func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()
		w.WriteHeader(http.StatusOK)
	})
	protectedHandler := authttp.PermissionRequired[string, testbackend.TestCredentials](permission)(protectedMux)

	mux.Handle("/protected/", http.StripPrefix("/protected", protectedHandler))

	listener, err := net.Listen(
		"tcp",
		"localhost:",
	)
	if err != nil {
		return nil, err
	}
	http.Serve(listener, muxChain)
}

See full mongodb example

Documentation

Index

Constants

View Source
const AuthContextString = "userLoginAuthSession"

Variables

View Source
var (
	ErrUnauthorized = errors.New("unauthorized")
	ErrInternal     = errors.New("internal error")
	ErrForbidden    = errors.New("forbidden")
)

Functions

func RequirePermission

func RequirePermission[P comparable, C any](
	ctx context.Context,
	permission P,
	authSession *AuthSession[P, C],
) error

Types

type AuthSession

type AuthSession[P comparable, C any] struct {
	User AuthUser
	// contains filtered or unexported fields
}

AuthSession, user-interaction type

func AuthRun

func AuthRun[P comparable, C any](
	ctx context.Context,
	store SessionStore,
	dataKey string,
	backend AuthzBackend[P, C],
) (*AuthSession[P, C], error)

func (*AuthSession[T, C]) Authenticate

func (aS *AuthSession[T, C]) Authenticate(
	ctx context.Context,
	creds C,
) (AuthUser, error)

Verifies the provided credentials via the backend returning the authenticated user if valid and otherwise `nil`.

func (*AuthSession[P, C]) Login

func (aS *AuthSession[P, C]) Login(c context.Context, user AuthUser) error

Updates the session such that the user is logged in.

func (*AuthSession[T, C]) Logout

func (aS *AuthSession[T, C]) Logout(c context.Context) AuthUser

Updates the session such that the user is logged out.

type AuthUser

type AuthUser interface {
	// Returns some identifying feature of the user.
	UserId() any

	// Returns a hash that's used by the session to verify the session is
	// valid.
	//
	// For example, if users have passwords, this method might return a
	// cryptographically secure hash of that password.
	SessionAuthHash() []byte
}

Authenticating user type.

type AuthnBackend

type AuthnBackend[C any] interface {
	// Authenticates the given credentials with the backend.
	Authenticate(ctx context.Context, creds C) (AuthUser, error)
	// Gets the user by provided ID from the backend.
	GetUser(ctx context.Context, userId any) (AuthUser, error)
}

A backend which can authenticate users.

Backends must implement:

  1. [AuthnBackend.Authenticate], a method for authenticating users with credentials and,
  2. [AuthnBackend.GetUser] a method for getting a user by an identifying feature.

With these two methods, users may be authenticated and later retrieved via the backend.

type AuthzBackend

type AuthzBackend[P comparable, C any] interface {
	AuthnBackend[C]
	// Gets the permissions for the provided user.
	GetUserPermissions(
		ctx context.Context,
		user AuthUser,
	) (mapset.Set[P], error)
	// Gets the group permissions for the provided user.
	GetGroupPermissions(
		ctx context.Context,
		user AuthUser,
	) (mapset.Set[P], error)
}

A backend which can authorize users.

Backends must implement AuthnBackend.

type Data

type Data struct {
	UserId any
	Hash   []byte
}

type SessionStore

type SessionStore interface {
	Get(c context.Context, k string) Data
	Set(c context.Context, k string, v Data)
	Save(c context.Context)
	Clear(c context.Context)
}

Directories

Path Synopsis
adapters
examples
sessions

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL