barong

package module
v0.0.0-...-bb3a97b Latest Latest
Warning

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

Go to latest
Published: May 8, 2025 License: MIT Imports: 16 Imported by: 0

README

Barong Session Service

This repository implements a session management service called barong in Go using the Gin framework, Watermill Redis for event publishing, and JWT tokens signed with ES256. It supports challenge–response authentication using Ethereum signatures (EIP‑712 style), access/refresh token issuance with rotation, and refresh token invalidation via Redis. Refresh tokens are valid for 5 days and access tokens for 5 minutes. Each refresh token is identified by a unique jti so that sessions on different devices or browsers can be invalidated independently.

Entities

The entities are types containing state and behavior, each JWT is repressented by a Token, A Session is composed of 2 tokens: Challenge and Refresh.

  • struct Token will represent 1 jwt token which use eth.Signer and jwt.Claims
  • struct Session will represent a session and holds 2 Tokens (Challenge and refresh)

Dependencies

  • github.com/layer-3/barong/internal/eth
  • github.com/redis/go-redis/v9
  • github.com/golang-jwt/jwt/v5
  • github.com/ThreeDotsLabs/watermill/message
  • github.com/gin-gonic/gin
  • github.com/google/uuid

Specifications

  • Challenge Token

    • Endpoint: POST /auth/challenge
    • Description: Issues a JWT challenge token containing a random nonce and a unique identifier (jti). Valid for 5 minutes.
  • Login

    • Endpoint: POST /auth/login
    • Description: The client signs the challenge token’s nonce with its Ethereum key and submits the challenge token, signature (hex encoded), and Ethereum address. The server validates the challenge token and signature, then issues an access token and a refresh token.
    • Tokens Issued:
      • Access Token: Valid for 5 minutes.
      • Refresh Token: Valid for 5 days.
  • Refresh

    • Endpoint: POST /auth/refresh
    • Description: The client submits its current refresh token. The server validates it and, if valid, invalidates the old refresh token (using its jti stored in Redis) and returns new access and refresh tokens.
  • Logout

    • Endpoint: POST /auth/logout
    • Description: Invalidates the provided refresh token (by marking its jti in Redis with a TTL matching its remaining lifetime) and publishes a logout event for cross-instance session synchronization.
  • Protected Endpoints

    • GET /api/me: Returns user information based on the access token.
    • GET /api/authorize: Returns 200 if the provided access token is valid.

File Structure

barong/
├── cmd/
│   └── barong/
│       └── main.go         # Application entry point.
│                              - Reads the REDIS_URL from the environment.
│                              - Connects to Redis (using go-redis).
│                              - Creates a Watermill Redis publisher.
│                              - Instantiates and starts the Gin server.
├── errors.go               # Defines common error variables used by the service.
├── interface.go            # Defines the public client interface for interacting with the session package.
├── service.go              # Implements HTTP handlers (login, refresh, logout, protected endpoints) and publishes events.
├── session.go              # Provides the Session abstraction that encapsulates access/refresh token logic,
│                              including validation, rotation, and invalidation via Redis (by token jti).
└── token.go                # Contains type Token and all low-level JWT creation and parsing logic (challenge, access, refresh tokens).
  • token.go: Contains the Token entity, and define 3 claims, NewToken(address string, TokenType, eth.Signer), the token will know it's type from the Claims it receives. Token object should be generic and can be either one of the token type by passing or registering the specific claims. Handles creation of JWT tokens for challenges, access, and refresh flows. All low-level JWT parsing including error handling related to signing method, claims, etc. Note: Do not generate a constructor for each Token Type only use NewToken

  • errors.go:
    Contains all the errors used across the package (e.g., token expiration, invalid signature, unexpected signing method).

  • session.go:
    Implements a Session struct that:

    • Define Session entity, represent the lifecycle of a user session by rotating and invalidating tokens.
    • Rotates refresh tokens by invalidating the old one in Redis (using its jti with a TTL equal to the token’s remaining lifetime).
    • Allows multiple sessions per user by invalidating tokens on a per‑jti basis.
    • When verifying a token validate the "aud" claim belong to the correct token type
    • Contains the 2 tokens that compose a session, challenge, refresh creating a higher level abstraction for service.go
    • Generate the access tokens
  • service.go:
    Provides the HTTP API endpoints by leveraging the Session. It also integrates with Watermill to publish logout events for cross-instance coordination.

  • interface.go:
    (Optional) Defines a Client interface that abstracts the session package’s external API, enabling easier testing or integration. Define a Store interface for storing refresh jti, Store interface are described below.

  • redis_store.go: Must implement Store taking a context in the constructor

  • memory_store.go: Should implement Store with a simple map for test purposes, we can ignore TTL

  • cmd/barong/main.go:
    Wires up the entire service by:

    • Reading the Redis connection URL from the environment.
    • Initializing the Redis client and Watermill publisher.
    • Generating (or loading) the ES256 signing key.
    • Starting the Gin router with the defined endpoints.

Token Descriptions

  • Challenge Token:

    • Claims:
      • aud: "session:challenge".
      • sub: User's Ethereum address.
      • jti: Unique token identifier.
      • exp: Expiration time (5 minutes after issuance).
      • nonce: A random string to be signed by the client.
  • Access Token:

    • Claims:
      • aud: "session:access".
      • sub: User's Ethereum address.
      • iat: Issued-at timestamp.
      • exp: Expiration time (5 minutes after issuance).
      • jti: Unique token identifier.
      • rid: Refresh token jti
  • Refresh Token:

    • Claims:
      • aud: "session:refresh".
      • sub: User's Ethereum address.
      • iat: Issued-at timestamp.
      • exp: Expiration time (5 days after issuance).
      • jti: Unique token identifier.
    • Invalidation:
      When a refresh token is used for rotation or logout, its jti is stored in Redis with a TTL equal to its remaining lifetime so that it is invalidated across all sessions.

Interfaces

The session package exposes a Client interface (defined in interface.go) that might look like:

type Client interface {
    // Challenge returns a challenge token.
    Challenge() (challenge Token, err error)
    // Login verifies the challenge token and signature, and returns new tokens.
    Login(challenge Token, signature, address string) (access, refresh Token, err error)
    // Refresh rotates the refresh token and returns new tokens.
    Refresh(refresh Token) (access, refresh Token, err error)
    // Logout invalidates the provided tokens.
    Logout(refresh, access Token) error
}

type Store interface {
  Set(ctx context.Context, key, value string, ttl time.Duration) error
  Get(ctx context.Context, key string) (string, error)
}

Routes Summary

Method Endpoint Description
POST /auth/challenge Issue a challenge token (nonce + jti, valid for 5 minutes).
POST /auth/login Verify challenge token and signature; issue access & refresh tokens.
POST /auth/refresh Rotate refresh token; issue new access & refresh tokens.
POST /auth/logout Invalidate refresh token and publish logout event.
GET /api/me Return user info based on access token claims.
GET /api/authorize Validate access token and confirm authorization.

Mermaid Sequence Diagram

Below is a Mermaid diagram that shows the high-level authentication flow:

sequenceDiagram
    participant C as Client
    participant S as Session Service
    participant R as Redis
    participant W as Watermill

    C->>S: POST /auth/challenge
    S-->>C: Returns Challenge Token (nonce, jti)
    C->>S: POST /auth/login<br/>(challenge token, signature, address)
    S->>S: Validate challenge token & signature
    S->>R: (Optional) Update session state (if needed)
    S-->>C: Returns access token (5 min) & refresh token (5 days)
    Note over C,S: Client uses access token for protected routes.
    C->>S: GET /api/me<br/>(with access token)
    S-->>C: Returns user info if token valid
    C->>S: POST /auth/refresh<br/>(with refresh token)
    S->>R: Invalidate current refresh token by jti (set key with TTL)
    S-->>C: Returns new access & refresh tokens
    C->>S: POST /auth/logout<br/>(with refresh token, access token)
    S->>R: Invalidate refresh token by jti
    S->>W: Publish logout event for synchronization
    S-->>C: Confirmation of logout

Documentation

Index

Constants

View Source
const (
	// TokenTypeChallenge represents a challenge token
	TokenTypeChallenge TokenType = "session:challenge"

	// TokenTypeAccess represents an access token
	TokenTypeAccess TokenType = "session:access"

	// TokenTypeRefresh represents a refresh token
	TokenTypeRefresh TokenType = "session:refresh"

	// DefaultChallengeExpiry is the default expiration time for challenge tokens
	DefaultChallengeExpiry = 5 * time.Minute

	// DefaultAccessExpiry is the default expiration time for access tokens
	DefaultAccessExpiry = 5 * time.Minute

	// DefaultRefreshExpiry is the default expiration time for refresh tokens
	DefaultRefreshExpiry = 120 * time.Hour // 5 days
)
View Source
const (
	// LogoutTopic is the topic for logout events
	LogoutTopic = "auth.logout"
)

Variables

View Source
var (
	// ErrTokenExpired is returned when a token has expired
	ErrTokenExpired = errors.New("token has expired")

	// ErrInvalidToken is returned when a token is invalid
	ErrInvalidToken = errors.New("invalid token")

	// ErrInvalidSignature is returned when a signature is invalid
	ErrInvalidSignature = errors.New("invalid signature")

	// ErrInvalidSigningMethod is returned when the signing method is not ES256
	ErrInvalidSigningMethod = errors.New("unexpected signing method")

	// ErrInvalidAudience is returned when the token audience is not as expected
	ErrInvalidAudience = errors.New("invalid audience")

	// ErrInvalidClaims is returned when the token claims are invalid
	ErrInvalidClaims = errors.New("invalid claims")

	// ErrTokenRevoked is returned when a token has been revoked
	ErrTokenRevoked = errors.New("token has been revoked")

	// ErrInvalidNonce is returned when the nonce is invalid
	ErrInvalidNonce = errors.New("invalid nonce")

	// ErrInvalidAddress is returned when the address is invalid
	ErrInvalidAddress = errors.New("invalid ethereum address")

	// ErrStoreOperationFailed is returned when a store operation fails
	ErrStoreOperationFailed = errors.New("store operation failed")

	// ErrSessionInvalid is returned when a session is invalid
	ErrSessionInvalid = errors.New("session is invalid")
)

Functions

This section is empty.

Types

type AccessClaims

type AccessClaims struct {
	jwt.RegisteredClaims
	RefreshID string `json:"rid,omitempty"`
}

AccessClaims represents the claims for an access token

type ChallengeClaims

type ChallengeClaims struct {
	jwt.RegisteredClaims
	Nonce string `json:"nonce"`
}

ChallengeClaims represents the claims for a challenge token

type Client

type Client interface {
	// Challenge returns a challenge token
	Challenge() (Token, error)

	// Login verifies the challenge token and signature, and returns new tokens
	Login(challenge Token, signature, address string) (access Token, refresh Token, err error)

	// Refresh rotates the refresh token and returns new tokens
	Refresh(refresh Token) (access Token, newRefresh Token, err error)

	// Logout invalidates the provided tokens
	Logout(refresh Token, access Token) error
}

Client represents the public interface for interacting with the session service

type EventPublisher

type EventPublisher interface {
	// Publish publishes an event to a topic
	Publish(topic string, data interface{}) error
}

EventPublisher represents an interface for publishing events

type LoginRequest

type LoginRequest struct {
	Challenge string `json:"challenge" binding:"required"`
	Signature string `json:"signature" binding:"required"`
	Address   string `json:"address" binding:"required"`
}

LoginRequest represents a login request

type LogoutEvent

type LogoutEvent struct {
	UserAddress string `json:"user_address"`
	TokenID     string `json:"token_id"`
}

LogoutEvent represents a logout event

type LogoutRequest

type LogoutRequest struct {
	RefreshToken string `json:"refresh_token" binding:"required"`
	AccessToken  string `json:"access_token" binding:"required"`
}

LogoutRequest represents a logout request

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore implements the Store interface using an in-memory map This is primarily intended for testing purposes

func NewMemoryStore

func NewMemoryStore(ctx context.Context) *MemoryStore

NewMemoryStore creates a new MemoryStore

func (*MemoryStore) Clear

func (s *MemoryStore) Clear()

Clear removes all data from the store This is useful for testing to reset the store between tests

func (*MemoryStore) Get

func (s *MemoryStore) Get(ctx context.Context, key string) (string, error)

Get retrieves a value by key

func (*MemoryStore) Set

func (s *MemoryStore) Set(ctx context.Context, key, value string, ttl time.Duration) error

Set stores a key with a value For MemoryStore, we ignore the TTL parameter as noted in the spec

type RedisStore

type RedisStore struct {
	// contains filtered or unexported fields
}

RedisStore implements the Store interface using Redis

func NewRedisStore

func NewRedisStore(ctx context.Context, redisURL string) (*RedisStore, error)

NewRedisStore creates a new RedisStore

func (*RedisStore) Close

func (s *RedisStore) Close() error

Close closes the Redis connection

func (*RedisStore) Get

func (s *RedisStore) Get(ctx context.Context, key string) (string, error)

Get retrieves a value by key

func (*RedisStore) GetClient

func (s *RedisStore) GetClient() *redis.Client

GetClient returns the Redis client This is used by the main application to share the Redis client with the Watermill publisher

func (*RedisStore) Set

func (s *RedisStore) Set(ctx context.Context, key, value string, ttl time.Duration) error

Set stores a key with a value and expiration time

type RefreshClaims

type RefreshClaims struct {
	jwt.RegisteredClaims
}

RefreshClaims represents the claims for a refresh token

type RefreshRequest

type RefreshRequest struct {
	RefreshToken string `json:"refresh_token" binding:"required"`
}

RefreshRequest represents a refresh request

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service provides HTTP handlers for the session service

func NewService

func NewService(store Store, signer eth.Signer, publisher message.Publisher) *Service

NewService creates a new service

func (*Service) Router

func (s *Service) Router() *gin.Engine

Router returns the gin router

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session represents a user session

func NewSession

func NewSession(store Store, signer eth.Signer) *Session

NewSession creates a new session

func (*Session) CreateChallenge

func (s *Session) CreateChallenge() (Token, error)

CreateChallenge creates a new challenge token

func (*Session) CreateTokens

func (s *Session) CreateTokens() (Token, Token, error)

CreateTokens creates new access and refresh tokens

func (*Session) InvalidateRefreshToken

func (s *Session) InvalidateRefreshToken(ctx context.Context, refreshToken Token) error

InvalidateRefreshToken invalidates a refresh token

func (*Session) RotateTokens

func (s *Session) RotateTokens(ctx context.Context, refreshToken Token) (Token, Token, error)

RotateTokens verifies the refresh token and creates new tokens

func (*Session) VerifyAccessToken

func (s *Session) VerifyAccessToken(ctx context.Context, accessToken Token) error

VerifyAccessToken verifies an access token

func (*Session) VerifyChallenge

func (s *Session) VerifyChallenge(ctx context.Context, challengeToken Token, signature string, address string) error

VerifyChallenge verifies a challenge token and its signature

type Store

type Store interface {
	// Set adds a key with a value and expiration time
	Set(ctx context.Context, key, value string, ttl time.Duration) error

	// Get retrieves a value by key
	Get(ctx context.Context, key string) (string, error)
}

Store represents the interface for storing and retrieving refresh token JTIs

type Token

type Token struct {
	// contains filtered or unexported fields
}

Token represents a JWT token

func NewToken

func NewToken(address string, tokenType TokenType, signer eth.Signer) (Token, error)

NewToken creates a new token based on the provided claims and signer

func ParseToken

func ParseToken(tokenString string, expectedType TokenType) (Token, error)

ParseToken parses a JWT string and returns a Token

func (Token) Claims

func (t Token) Claims() jwt.Claims

Claims returns the token claims

func (Token) GetExpiresAt

func (t Token) GetExpiresAt() (time.Time, error)

GetExpiresAt returns the expiration time of the token

func (Token) GetJTI

func (t Token) GetJTI() (string, error)

GetJTI returns the JTI from a token

func (Token) GetNonce

func (t Token) GetNonce() (string, error)

GetNonce returns the nonce from a challenge token

func (Token) GetRefreshID

func (t Token) GetRefreshID() (string, error)

GetRefreshID returns the refresh ID from an access token

func (Token) GetSubject

func (t Token) GetSubject() (string, error)

GetSubject returns the subject from a token

func (*Token) SetRefreshID

func (t *Token) SetRefreshID(refreshID string) error

SetRefreshID sets the refresh ID for an access token

func (Token) String

func (t Token) String() string

String returns the JWT string representation of the token

func (Token) Type

func (t Token) Type() TokenType

Type returns the token type

func (Token) Validate

func (t Token) Validate() error

Validate validates the token

type TokenResponse

type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token,omitempty"`
}

TokenResponse represents a token response

type TokenType

type TokenType string

TokenType represents the type of token

type UserResponse

type UserResponse struct {
	Address string `json:"address"`
}

UserResponse represents a user response

Directories

Path Synopsis
cmd
barong command
internal
eth

Jump to

Keyboard shortcuts

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