authress

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 12, 2024 License: MIT Imports: 22 Imported by: 0

README

Authress

Authress is a lightweight Go package for OAuth 2.0 / OpenID Connect (OIDC) resource server token authentication using JWT RFC 7519 and Token Introspection. It lets you verify tokens issued by an authorization server (like Auth0, Keycloak, etc.) and used against your API (resource server).

Features
  • OAuth2/OIDC Discovery: Automatic fetching and handling of JWKS based on auth server discovery.
  • JWT Validation and Parsing:
  • Token Introspection: Supports token introspection for cases where you need to verify token status directly with the authorization server.
  • Middleware: Provides middleware for validating JWTs in HTTP requests.
Installation
go get https://github.com/theadell/authress
Examples
Middleware
package main

import (
	"log"
	"net/http"
	"os"

	"github.com/justinas/alice"        
	"github.com/theadell/authress"
)

func main() {

	IdpURL := "https://idp.com/.well-known/openid-configuration"

	validator, err := authress.New(
		authress.WithDiscovery(IdpURL),
		authress.WithIntrospection("clientID", "clientSecret"))
	if err != nil {
		log.Fatalf("Failed to create validator: %v", err)
	}

	// Enforce JWT validation 
	requireJWT := authress.RequireAuthJWT(validator)
	http.Handle("/secure", requireJWT(http.HandlerFunc(handler1)))

	// Enforce introspection validation
	requireIntrospection := authress.RequireAuthWithIntrospection(validator)
	http.Handle("/important", requireIntrospection(http.HandlerFunc(handler2)))

	// Inject JWT into context without enforcing authentication
	setAuthContext := authress.SetAuthContextJWT(validator)
	chain := alice.New(setAuthCtx, setContextLDAP, enforceAuth, authorize).
		Then(http.HandlerFunc(handler3))
	http.Handle("/another-route", chain)

	log.Fatal(http.ListenAndServe(":8080", nil))
}
Customize Middlware
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"

	"github.com/theadell/authress"
)

// Custom Token Extractor: Extract JWT from a custom header (or cookie, etc.)
func myTokenExtractor(r *http.Request) string {
	cookie, err := r.Cookie("token")
	if err != nil {
		return "" 
	}
	return cookie.Value
}

// Custom Context Modifier: Inject custom values into the request context 
func myContextModifier(ctx context.Context, token *authress.Token) context.Context {
	return context.WithValue(ctx, "userRole", token.GetStringClain("role"))
}

// Custom Error Responder: Customize the error response when validation fails
func myErrorResponder(w http.ResponseWriter, r *http.Request, err error) {
	http.Error(w, "Custom Unauthorized: "+err.Error(), http.StatusUnauthorized)
}

func main() {
//...

	requireJWT := authress.RequireAuthJWT(
		validator,
		authress.WithTokenExtractor(myTokenExtractor),       
		authress.WithContextModifier(myContextModifier),     
		authress.WithErrorResponder(myErrorResponder),       
	)

	http.Handle("/secure", requireJWT(http.HandlerFunc(myHandler))

	log.Fatal(http.ListenAndServe(":8080", nil))
}

Validation And parsing

You can also use the validator directly

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/theadell/authress"
)

func main() {
	// Create a Validator using OAuth2/OIDC discovery
	validator, err := authress.New(
		authress.WithAuthServerDiscovery(IdpUrl))
	if err != nil {
		log.Fatalf("Failed to create validator: %v", err)
	}

	http.HandleFunc("/secure", func(w http.ResponseWriter, r *http.Request) {
		tokenString := extractToken(r)

		token, err := validator.ValidateJWT(tokenString)
		if err != nil {
			http.Error(w, "Invalid token", http.StatusUnauthorized)
			return
		}
        // do sth with the token 
        user, err := db.GetUserBySubject(token.Sub())
		// ... 
	})

    http.HandleFunc("/parse", func(w http.ResponseWriter, r *http.Request) {
		tokenString := extractToken(r)

		token, err := validator.Parse(tokenString)
		if err != nil {
			http.Error(w, "Invalid token", http.StatusUnauthorized)
			return
		}
        // do sth with the token 
        role := token.GetStringClaim("role")
        email := token.Email
		pic := token.Picture
		var claim MyCustomClaimType
		token.GetClaimAs("MyCustomClaimKey", &claim)
		// ... 
	})
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Using Your Own Cryptographic Keys

You can provide custom metadata and keys to the validator instead of relying on OAuth2/OIDC discovery.

Custom Metadata To use your own OAuth2 server metadata, pass it via WithMetadata:

metadata := &authress.OAuth2ServerMetadata{
    Issuer: "https://your-issuer.com",
    // other metadata fields
}
validator, _ := authress.New(authress.WithMetadata(metadata))

Custom JWKS

type MyJWKSStore struct{}

func (s *MyJWKSStore) GetKey(ctx context.Context, kid string) (crypto.PublicKey, error) {
    // Retrieve key by 'kid'
    return myKey, nil
}

validator, _ := authress.New(authress.WithJWKS(&MyJWKSStore{}), authress.WithMetadata(metadata))

Documentation

Overview

Authress is a lightweight package for adding token validation and introspection to resource servers. It supports OAuth2/OIDC discovery and custom keys & JWT parsing and validation the package also provides easily customizable and pluggable middleware for integrating authentication into net/http handlers.

See the README for usage and examples.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidJWT           = errors.New("token format is not valid")
	ErrInvalidSignature     = errors.New("token format is not valid")
	ErrUnsupportedAlgorithm = errors.New("unsupported signing algorithm")
	ErrInvalidPublicKey     = errors.New("invalid or unsupported public key")
	ErrInvalidIssuer        = errors.New("invalid issuer")
	ErrTokenExpired         = errors.New("token has expired")
	ErrTokenNotYetValid     = errors.New("token is not yet valid; nbf > time.Now()")
	ErrInvalidAudience      = errors.New("invalid audience claim")
	ErrCalimNotExists       = errors.New("claim doesn't exist")
	ErrDiscoveryFailure     = errors.New("failed to discover OAuth2 server metadata")
)
View Source
var AuthContextKey = contextKey("x-authenticated")

AuthContextKey is the key used to store authentication information in the request context.

Functions

func RequireAuthJWT

func RequireAuthJWT(v *Validator, opts ...MiddlewareOption) func(http.Handler) http.Handler

RequireAuthJWT requires that a valid JWT is present in the request. If the JWT is invalid or missing, the request is rejected with HTTP 401 Unauthorized. If the JWT is valid, the parsed token is injected into the request context.

The JSON Web Token is extracted from the `Authorization` header using the Bearer scheme by default. You can customize the token extraction method using the WithTokenExtractor option, such as extracting the token from a cookie or a custom header.

if the JWT is valid, the request context is modified to include the parsed token and a flag indicating whether the request is authenticated. See the WithContextModifier option

If the token is invalid or missing, the middleware responds with a 401 status and a default "Unauthorized" message. The error response can be customized using WithErrorResponder option.

Example Usage:

requireJWT := RequireAuthJWT(validator)
http.Handle("/secure", requireJWT(http.HandlerFunc(secureHandler)))
http.ListenAndServe(":8080", nil)

// Custom token extraction from a cookie and custom error response
requireJWT := RequireAuthJWT(validator,
    WithTokenExtractor(func(r *http.Request) string {
        cookie, err := r.Cookie("token")
        if err != nil {
            return ""
        }
        return cookie.Value
    }),
    WithErrorResponder(func(w http.ResponseWriter, r *http.Request, err error) {
        http.Error(w, "Custom unauthorized message", http.StatusUnauthorized)
    }))

func RequireAuthWithIntrospection

func RequireAuthWithIntrospection(v *Validator, opts ...MiddlewareOption) func(http.Handler) http.Handler

RequireAuthWithIntrospection requires a valid JWT by token introspection as defined by RFC 7662. Inrospection introduces network latency as it requires an HTTP roundtrip. useful for critical endpoints where ensuring that the token has not been revoked is important. In Most cases RequireAuthJWT middlware should be preferred

Example Usage:

requireAuth := RequireAuthWithIntrospection(introspectionValidator)
http.Handle("/important", requireAuth(http.HandlerFunc(importantHandler)))

http.ListenAndServe(":8080", nil)

func SetAuthContextJWT

func SetAuthContextJWT(v *Validator, opts ...MiddlewareOption) func(http.Handler) http.Handler

SetAuthContextJWT validates the JWT and injects the parsed token into the request context WITHOUT enforcing authentication. The Authentication decision is left to downstream middleware / handlers.

The JWT is extracted from the `Authorization` header using the Bearer scheme by default, See WithTokenExtractor option. You can modify how the context is updated using the WithContextModifier option.

Example Usage:

chain := alice.New(setAuthContextJWT, setAuthContextLDAP, enforceAuth, authorize).Then(http.HandlerFunc(secureHandler))
http.Handle("/secure", chain)
http.ListenAndServe(":8080", nil)

func SetAuthCtxhWithIntrospection

func SetAuthCtxhWithIntrospection(v *Validator, opts ...MiddlewareOption) func(http.Handler) http.Handler

SetAuthCtxWithIntrospection validates the token via introspection (RFC 7662) but DOES NOT ENFORCE authentication. It adds the token and its status to the context for downstream use.

Example:

v, err := authress.NewValidator(
		authress.WithAuthServerDiscovery(kcDiscoveryUrl),
		authress.WithIntrospection(clientID, clientSecret))
if err != nil {
	panic(err) // handle error
}
setAuthCtx := SetAuthCtxWithIntrospection(v)
chain := alice.New(setAuthCtx, setContextLDAP, enforceAuth, authorize).Then(http.HandlerFunc(secureHandler))
http.Handle("/endpoint", chain)

http.ListenAndServe(":8080", nil)

Types

type AuthCtx

type AuthCtx struct {
	IsAuthenticated bool
	Token           *Token
}

AuthCtx holds information about the authenticated request.

func GetAuthCtx

func GetAuthCtx(ctx context.Context) (*AuthCtx, bool)

type Claims added in v0.1.1

type Claims struct {

	// Audience (aud) is the token's intended recipients.
	Audience audience `json:"aud"`

	// Issuer (iss) is the entity that issued the token.
	Issuer string `json:"iss"`

	// Subject (sub) is the principal the token is issued for.
	Subject string `json:"sub"`

	// ExpiresAt (exp) is the token's expiration time.
	ExpiresAt int64 `json:"exp"`

	// IssuedAt (iat) is when the token was issued.
	IssuedAt int64 `json:"iat"`

	// NotBefore (nbf) indicates when the token becomes valid.
	NotBefore int64 `json:"nbf"`

	// optional OIDC claim, zero value if absent
	Name string `json:"name"`

	// optional OIDC claim, zero value if absent
	GivenName string `json:"given_name"`

	// optional OIDC claim, zero value if absent
	FamilyName string `json:"family_name"`

	// optional OIDC claim, zero value if absent
	Email string `json:"email"`

	// optional OIDC claim, zero value if absent
	EmailVerified bool `json:"email_verified"`

	// optional OIDC claim, zero value if absent
	Picture string `json:"picture"`

	// optional OIDC claim, zero value if absent
	Scope string `json:"scope"`
	// contains filtered or unexported fields
}

Claims represent JWT data. Registered claims (RFC 7519) and common OIDC claims are available as fields. Missing claims default to zero values Additional claims can be accessed lazily with Claims.GetClaim.

func (*Claims) ExpiresAtTime added in v0.1.1

func (c *Claims) ExpiresAtTime() time.Time

ExpiresAtTime returns the `exp` time as time.Time

func (*Claims) GetClaim added in v0.1.1

func (c *Claims) GetClaim(key string) (any, bool)

GetClaim returns a calim by its key

func (*Claims) GetClaimAs added in v0.1.1

func (c *Claims) GetClaimAs(key string, v interface{}) error

GetClaimAs retrieves the claim by `key` and unmarshals it into `v`, which must be a non-nil pointer. It uses reflection to dynamically map the claim to the provided type.

This involves unmarshaling the raw JSON, which can be costly.

Returns an error if the key is empty or if `v` is not a valid pointer.

func (*Claims) GetIntClaim added in v0.1.1

func (c *Claims) GetIntClaim(key string) int64

GetIntClaim retrieves the value of a claim by its key as a int64. If the claim is missing or not numeric, zero returned.

func (*Claims) GetStringClaim added in v0.1.1

func (c *Claims) GetStringClaim(key string) string

GetStringClaim retrieves the value of a claim by its key as a string. If the claim is missing or not a string, an empty string is returned.

func (*Claims) IssuedAtTime added in v0.1.1

func (c *Claims) IssuedAtTime() time.Time

IssuedAtTime returns the token's `iat` time as a time.Time

func (*Claims) NotBeforeTime added in v0.1.1

func (c *Claims) NotBeforeTime() time.Time

NotBeforeTime returns the token's `nbf` time as a time.Time

func (*Claims) Scopes added in v0.1.1

func (c *Claims) Scopes() []string

Scopes returns the token's scopes

type ContextModifier

type ContextModifier func(ctx context.Context, token *Token, valid bool) context.Context

ContextModifier modifies the request context, adding authentication details if the token is valid.

type ErrorResponder

type ErrorResponder func(w http.ResponseWriter, r *http.Request, err error)

ErrorResponder handles the HTTP response when token validation fails (e.g., sending a 401 Unauthorized).

type JWKSStore

type JWKSStore interface {
	GetKey(ctx context.Context, kid string) (crypto.PublicKey, error)
}

type MiddlewareOption

type MiddlewareOption func(*middlewareOptions)

func WithContextModifier

func WithContextModifier(modifier ContextModifier) MiddlewareOption

WithContextModifier sets a custom modifer of the request context

func WithErrorResponder

func WithErrorResponder(responder ErrorResponder) MiddlewareOption

WithErrorResponder sets custom error response handling

func WithTokenExtractor

func WithTokenExtractor(extractor TokenExtractor) MiddlewareOption

WithContextModifier sets custom JWT extractor from the request context based on authentication.

type OAuth2ServerMetadata

type OAuth2ServerMetadata struct {
	Issuer                            string   `json:"issuer"`
	AuthEndpoint                      string   `json:"authorization_endpoint"`
	TokenEndpoint                     string   `json:"token_endpoint"`
	DeviceAuthEndpoint                string   `json:"device_authorization_endpoint"`
	JWKURI                            string   `json:"jwks_uri"`
	IntrospectionEndpoint             string   `json:"introspection_endpoint"`
	IntrospectionAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported"`
	RevocationEndpoint                string   `json:"revocation_endpoint"`
	RevocationAuthMethodsSupported    []string `json:"revocation_endpoint_auth_methods_supported"`
	ScopesSupported                   []string `json:"scopes_supported"`
	ResponseTypesSupported            []string `json:"response_types_supported"`
	GrantTypesSupported               []string `json:"grant_types_supported"`
	TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
	SubjectTypesSupported             []string `json:"subject_types_supported"`
	IDTokenSigningAlgsSupported       []string `json:"id_token_signing_alg_values_supported"`
	CodeChallengeMethodsSupported     []string `json:"code_challenge_methods_supported"`
}

func (*OAuth2ServerMetadata) Endpoint

func (m *OAuth2ServerMetadata) Endpoint() oauth2.Endpoint

type Option

type Option func(*config)

func WithAudienceValidation

func WithAudienceValidation(audience ...string) Option

WithAudienceValidation enables or disables audience validation and sets the expected audience.

func WithDiscovery

func WithDiscovery(discoveryUrl string) Option

func WithHTTPClient

func WithHTTPClient(client *http.Client) Option

WithHTTPClient sets a custom HTTP client for fetching JWKS and introspection.

func WithIntrospection

func WithIntrospection(clientId, clientSecret string) Option

WithIntrospection enables token introspection.

func WithJWKS

func WithJWKS(jwks JWKSStore) Option

func WithMetadata

func WithMetadata(metadata *OAuth2ServerMetadata) Option

type Token

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

func (*Token) Alg

func (t *Token) Alg() string

func (*Token) Kid added in v0.1.1

func (t *Token) Kid() string

type TokenExtractor

type TokenExtractor func(r *http.Request) string

TokenExtractor extracts a token from the HTTP request (e.g., from a header or cookie).

func BearerTokenExtractor

func BearerTokenExtractor() TokenExtractor

BearerTokenExtractor extracts the token from the Authorization header (Bearer token).

func CookieTokenExtractor

func CookieTokenExtractor(cookieName string) TokenExtractor

CookieTokenExtractor extracts a token from a cookie.

func CustomHeaderTokenExtractor

func CustomHeaderTokenExtractor(headerName string) TokenExtractor

CustomHeaderTokenExtractor extracts a token from a custom HTTP header.

type Validator

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

func New

func New(options ...Option) (*Validator, error)

func (*Validator) ClientEndpoint

func (v *Validator) ClientEndpoint() oauth2.Endpoint

ClientEndpoint return `x/oauth2` client Endpoint

func (*Validator) IntrospectToken

func (v *Validator) IntrospectToken(ctx context.Context, token string) (bool, error)

IntrospectToken checks if the provided token is active by querying the introspection endpoint according to RFC 7662. Most users will prefer using Validator.ValidateJWT for local validation to avoid the network latency of introspection. Introspection is useful for opaque tokens or when you need to confirm if a token has been revoked.

func (*Validator) Parse

func (v *Validator) Parse(tokenString string) (*Token, error)

Parse parses and returns the JWT Token or error if the token is not strucrually valid JWT

func (*Validator) ValidateJWT

func (v *Validator) ValidateJWT(tokenString string) (*Token, error)

ValidateJWT checks if the given JWT is valid by verifying its signature and standard claims. Returns a Token object on success or an error if validation fails.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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