frontdex

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Dec 29, 2025 License: MIT Imports: 19 Imported by: 0

README

frontdex

OAuth2/OIDC middleware that fronts Dex

Go Reference Go Report Card

frontdex provides OAuth2/OIDC authentication middleware for applications that use Dex as the identity provider. It handles the authorization code flow (with PKCE) and stores tokens and claims in the request context for further processing.

Quickstart

1. Start Dex

Create a Dex config file dex.yaml like this:

# The URL where your app is reachable, not Dex itself
issuer: http://localhost:8080/login

# See https://dexidp.io/docs/storage/ for other storage options
storage:
  type: memory

web:
  http: 0.0.0.0:5556 # Dex will listen on this port
  clientRemoteIP:
    header: X-Forwarded-For # frontdex sets this header

oauth2:
  grantTypes:
    - "authorization_code"
  responseTypes: [ "code" ]
  skipApprovalScreen: true # must be set

staticClients:
- id: example-client
  redirectURIs:
  - 'http://localhost:8080/login/callback'
  name: 'Example client'
  secret: change-me-in-production

# See https://dexidp.io/docs/connectors/ for other connectors
connectors:
- type: github
  id: github
  name: GitHub
  config:
    clientID: YOUR_OAUTH_CLIENT_ID
    clientSecret: YOUR_OAUTH_CLIENT_SECRET
    redirectURI: http://localhost:8080/login/callback # your OAuth app's callback URL
- type: mockCallback # for testing without external IdP
  id: mock
  name: Mock

logger:
  level: "debug"
  format: "text"

Here, we enable the GitHub connector and a mock connector for testing. To use the GitHub connector, you need to register a new OAuth app on GitHub developer settings.

Start Dex with Docker:

docker run \
  --name dex \
  -p 5556:5556 \
  -v ./dex.yaml:/etc/dex/config.yaml \
  --rm \
  dexidp/dex:latest \
  dex serve /etc/dex/config.yaml
2. Create your app

Install:

go get github.com/tetsuo/frontdex

By default, frontdex connects the Dex instance running at localhost:5556. You can configure it to point elsewhere using the WithEndpointURL() option.

📄 See the API documentation at pkg.go.dev for all available options.

There aren't many options to configure to test it out locally. Here's a minimal example app:

package main

import (
	"encoding/json"
	"net/http"

	"github.com/tetsuo/frontdex"
)

func main() {
	fdx := frontdex.New(
		"http://localhost:8080/login",    // issuer URL
		"example-client",                 // client ID
		"change-me-in-production",        // client secret
		frontdex.WithCookieSecure(false), // for local testing
	)

	http.ListenAndServe(":8080", http.StripPrefix("/login", fdx(
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if payload := frontdex.Token(r); payload != nil {
				// User is logged in at /callback; payload contains the JWT and user info
				w.Header().Set("Content-Type", "application/json")
				_ = json.NewEncoder(w).Encode(payload)
			}
		}),
	)))
}

Documentation

Overview

Package frontdex provides OAuth2/OIDC authentication middleware for applications using Dex as the identity provider and supports the authorization code flow.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoState is returned when the OAuth2 state parameter is missing from the callback request.
	ErrNoState = errors.New("state missing")
	// ErrBadError is returned when an unrecognized error was returned in the OAuth2 callback.
	ErrBadError = errors.New("bad error")
	// ErrMissingStateToken is returned when the state token cookie is missing from the request.
	ErrMissingStateToken = errors.New("state token missing")
	// ErrBadStateToken is returned when the state token is invalid, tampered, or expired.
	ErrBadStateToken = errors.New("bad state token")
	// ErrAccessDenied is returned when the OAuth provider returns an "access_denied" or "unverified_user_email" error.
	// access_denied is standard, unverified_user_email is used by some providers like GitHub.
	ErrAccessDenied = errors.New("access denied")
	// ErrStateMismatch is returned when the state parameter in the callback doesn't match the initial state,
	// the value stored in the state token.
	ErrStateMismatch = errors.New("state mismatch")
)

Errors.

View Source
var (
	// ErrBadConnector is returned when the connector field (via) in the request is invalid or not supported.
	ErrBadConnector = dex.ErrInvalidConnector
	// ErrResourceUnavailable is returned when a required resource (state) is not found on Dex.
	ErrResourceUnavailable = dex.ErrResourceUnavailable
	// ErrAuthFailure is returned when authentication failed at the Dex server.
	ErrAuthFailure = dex.ErrAuthFailure
	// ErrTimeout is returned when a request to Dex times out.
	ErrTimeout = dex.ErrTimeout
	// ErrNetwork is returned when a network error occurs while communicating with Dex.
	ErrNetwork = dex.ErrNetwork
)

Errors returned from the Dex client.

View Source
var (
	// ErrorHandler sends an appropriate HTTP error response based on the failure reason.
	// Used when an error occurs during the authentication process. Override with [WithErrorHandler].
	ErrorHandler = http.HandlerFunc(errorHandler)
	// RedirectHandler sets the state token cookie and redirects to the authorization URL.
	// Used to initiate the authentication process. Override with [WithRedirectHandler].
	RedirectHandler = http.HandlerFunc(redirectHandler)
)

Default request handlers.

Functions

func AuthorizationURL

func AuthorizationURL(r *http.Request) string

AuthorizationURL retrieves the OAuth2 authorization URL from the request context. This value is only present in redirect handler.

func FailureReason

func FailureReason(r *http.Request) error

FailureReason retrieves the error value from the request context. This value is only present in error handler.

func New

func New(issuerURL, clientID, clientSecret string, opts ...Option) func(http.Handler) http.Handler

New returns an http.Handler that provides OAuth2/OIDC authentication using Dex as the identity provider. It intercepts requests to / and /callback to handle the authorization and callback flows, respectively. All other requests are passed to the provided handler h. You should mount this handler under a dedicated path, such as /login/.

The issuerURL, clientID, and clientSecret parameters configure the OAuth2 client and must match the values registered with your Dex server. Additional options, such as a custom Dex endpoint URL, can be provided using functional options.

Note: set issuerURL to your application (for example, https://example.com/login), not to to Dex. Dex uses the issuer URL to validate redirect URIs, but its discovery document (/.well-known/openid-configuration) will advertise /keys, /token, and other endpoints under your application's URL. If your application tried to call those endpoints, it would end up calling itself. That's why frontdex does not use OIDC discovery; it constructs the required endpoints itself and talks directly to Dex (which must be reachable from the application server). By default, frontdex assumes Dex is at http://localhost:5556; if Dex runs elsewhere, use WithEndpointURL to set the correct Dex base URL.

Upon successful authentication, the handler stores the authentication payload in the request context. You can retrieve it using the Token function. The payload is only present in GET /callback requests after successful authentication.

If authentication fails, the error is saved in the request context. Access it via FailureReason within the error handler; only requests routed to the ErrorHandler include this value. You can customize the handler with the WithErrorHandler option (recommended).

Example usage:

import "github.com/tetsuo/frontdex"

fdx := frontdex.New(
  "https://example.com/login",             // issuer URL
  "example-client",                        // client ID
  "change-me-in-production",               // client secret
  frontdex.WithLoginHandler(loginHandler), // custom login page
)
http.ListenAndServe(":8080", http.StripPrefix("/login", fdx(
  http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if payload := frontdex.Token(r); payload != nil {
      w.Header().Set("Content-Type", "application/json")
      _ = json.NewEncoder(w).Encode(payload)
    }
  }),
)))

func StateToken

func StateToken(r *http.Request) *http.Cookie

StateToken retrieves the state token cookie from the request context. This value is only present in redirect handler.

func StatusCodeFromError

func StatusCodeFromError(err error) int

StatusCodeFromError maps known authentication and API errors to appropriate HTTP status codes for use in HTTP responses. Returns 400, 403, or 500 depending on the error type.

Types

type Connector

type Connector = dex.Connector

Connector represents a Dex connector ID.

type Crypto

type Crypto interface {
	// Decrypt decrypts the given data.
	Decrypt(data []byte) ([]byte, error)
	// Encrypt encrypts the given data.
	Encrypt(data []byte) ([]byte, error)
}

Crypto defines the interface for encrypting and decrypting state tokens.

type Option

type Option func(*frontdex)

Option configures a frontdex instance.

func WithClientRemoteIPHeader

func WithClientRemoteIPHeader(headerName string) Option

WithClientRemoteIPHeader sets the name of the header sent to Dex containing the client's IP address. This must match the clientRemoteIP.header value in the Dex configuration. Defaults to "X-Forwarded-For".

func WithConnectorFieldName

func WithConnectorFieldName(name string) Option

WithConnectorFieldName sets the form field name used to obtain the connector ID. Defaults to "via".

func WithConnectors

func WithConnectors(connectors []string) Option

WithConnectors restricts authentication to the specified Dex connector IDs. Only these connectors will be allowed when sending requests to Dex.

func WithCookieDomain

func WithCookieDomain(domain string) Option

WithCookieDomain sets the domain attribute for cookies provided to clients during the OAuth2 authentication flow. Defaults to the current request domain (recommended). The value is treated as being prefixed with a '.', so "example.com" also matches subdomains like "www.example.com".

func WithCookieHttpOnly

func WithCookieHttpOnly(httpOnly bool) Option

WithCookieHttpOnly toggles the 'HttpOnly' flag on cookies provided to clients during the OAuth2 authentication flow. Defaults to true.

func WithCookieMaxAge

func WithCookieMaxAge(age int) Option

WithCookieMaxAge sets the maximum age (in seconds) for cookies provided to clients as part of the OAuth2 authentication flow. Must match Dex's expiry.authRequests. Defaults to 24h.

func WithCookieName

func WithCookieName(name string) Option

WithCookieName sets the name of the cookie provided to clients as part of the OAuth2 authentication flow. Cookie names must not contain whitespace, commas, semicolons, backslashes, or control characters, as per RFC 6265. Default value is "_fdx_chal".

func WithCookiePath

func WithCookiePath(path string) Option

WithCookiePath sets the path for cookies provided to clients during the OAuth2 authentication flow. Defaults to the path the cookie was issued from (recommended).

func WithCookieSameSite

func WithCookieSameSite(sameSite SameSiteMode) Option

WithCookieSameSite sets the 'SameSite' attribute for cookies provided to clients during the OAuth2 authentication flow. Defaults to SameSiteLaxMode.

func WithCookieSecure

func WithCookieSecure(secure bool) Option

WithCookieSecure toggles the 'Secure' flag on cookies provided to clients during the OAuth2 authentication flow. Defaults to true; disable only for local HTTP development.

func WithCookieTrustedOrigins

func WithCookieTrustedOrigins(origins []string) Option

WithCookieTrustedOrigins registers Referer origins that are treated as trusted when handling cookies during the OAuth2 authentication flow. Only include origins you own or fully control.

func WithCustomCrypto

func WithCustomCrypto(cyp Crypto) Option

WithCustomCrypto injects a custom Crypto implementation for encrypting and decrypting state tokens. By default AES is used with a random 32-byte key. Setting a custom Crypto overrides any state secret set via WithStateSecret or WithStateSecretHex.

func WithCustomTransport

func WithCustomTransport(rt http.RoundTripper) Option

WithCustomTransport sets the custom HTTP transport for requests sent to Dex.

func WithEndpointURL

func WithEndpointURL(endpoint string) Option

WithEndpointURL sets the base URL for the Dex server and configures the OAuth2 endpoints (auth, token), JWKS (keys), and userinfo URLs accordingly. Note that frontdex doesn't use the discovery protocol to obtain these URLs. Defaults to http://localhost:5556.

func WithErrorHandler

func WithErrorHandler(h http.Handler) Option

WithErrorHandler overrides the default error handler for OAuth callbacks. By default, a simple text error response is returned. Note that internal server errors (500) might contain sensitive information. It's recommended to log such errors instead of displaying them to users.

For production use, consider implementing a custom error handler. For example:

WithErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    err := frontdex.FailureReason(r)
    statusCode := frontdex.StatusCodeFromError(err)
    if statusCode == http.StatusInternalServerError {
        // Log the error details internally
        log.Printf("Internal error: %v (status: %d)", err, statusCode)
        // Return a generic error message to the user
        http.Error(w, "Something went wrong", statusCode)
    } else {
        http.Error(w, err.Error(), statusCode)
    }
}))

func WithHTTPClientTimeout

func WithHTTPClientTimeout(timeout time.Duration) Option

WithHTTPClientTimeout sets the timeout duration for requests sent to Dex. Defaults to 30s.

func WithLoginHandler

func WithLoginHandler(h http.Handler) Option

WithLoginHandler overrides the default login page handler. By default, a simple HTML page with buttons for each available connector is served. The form fields use the configured connector field name (default: "via"). If you implement a custom login page (which you should), ensure that the form field names match, or adjust the connector field name using WithConnectorFieldName.

func WithOAuthRedirectURL

func WithOAuthRedirectURL(redirectURL string) Option

WithOAuthRedirectURL sets the OAuth2 redirect URL used for callbacks. Must match with connector redirect URI configuration in Dex. Defaults to issuerURL + "/callback".

func WithOAuthScopes

func WithOAuthScopes(scopes []string) Option

WithOAuthScopes sets the OAuth2 scopes requested from Dex during login. Default: openid, profile, email, federated:id.

func WithRedirectHandler

func WithRedirectHandler(h http.Handler) Option

WithRedirectHandler overrides the default redirect handler. By default, the handler sets the state token cookie and redirects to the authorization URL. For custom behavior, implement your own handler (recommended for validating auth URLs or adding additional security checks).

You can access the state token and authorization URL using StateToken and AuthorizationURL. The default handler logic is as follows:

WithRedirectHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Set the state token cookie
    http.SetCookie(w, frontdex.StateToken(r))

    // Redirect to the authorization URL
    http.Redirect(w, r, frontdex.AuthorizationURL(r), http.StatusFound)
}))

For example, add CSRF protection:

import "github.com/gorilla/csrf"

protect := csrf.Protect(...)

opts := []frontdex.Option{
    frontdex.WithRedirectHandler(protect(frontdex.RedirectHandler)),
    frontdex.WithLoginHandler(protect(yourLoginHandler)),
}

func WithStateSecret

func WithStateSecret(key []byte) Option

WithStateSecret specifies the key used by the built-in AES cipher to encrypt and decrypt state tokens. The key must be 16, 24, or 32 bytes long; if not set, a random 32-byte key is used by default.

func WithStateSecretHex

func WithStateSecretHex(hexKey string) Option

WithStateSecretHex sets the state secret key using a hex-encoded string. The decoded key must be exactly 16, 24, or 32 bytes in length.

func WithTokenTTL

func WithTokenTTL(d time.Duration) Option

WithTokenTTL configures the expected lifetime of the ID token issued by Dex. Used for validation. Must match Dex's expiry.idToken. Defaults to 24h.

func WithTrustedPeerCIDRs

func WithTrustedPeerCIDRs(cidrs []netip.Prefix) Option

WithTrustedPeerCIDRs sets the IP ranges that are allowed to send proxy headers.

This option is REQUIRED to accept "X-Forwarded-For" headers and forward them to Dex. Only requests where RemoteAddr matches one of these CIDRs will have their headers forwarded. This should be set to the IP range of your load balancer in your trusted network.

Note that proxy header parsing is complex and out of scope for this library. frontdex only provides basic support for obtaining a single client IP from the "X-Forwarded-For" header, ignoring comma-separated IPs.

Example: For a load balancer at 10.0.0.0/24:

frontdex.New(issuerURL, clientID, clientSecret,
    frontdex.WithTrustedPeerCIDRs([]netip.Prefix{
        netip.MustParsePrefix("10.0.0.0/24"),
    }),
)

type Payload

type Payload struct {
	AccessToken  string      `json:"access_token"`
	IDToken      string      `json:"id_token"`
	TokenType    string      `json:"token_type"`
	ExpiresIn    int         `json:"expires_in"`
	RefreshToken string      `json:"refresh_token,omitempty"`
	Claims       *dex.Claims `json:"claims"`
}

Payload is the authentication payload returned after successful login.

func Token added in v0.2.0

func Token(r *http.Request) *Payload

Token retrieves the authentication payload from the request context. This value is only present after successful authentication on /callback.

type SameSiteMode

type SameSiteMode int

SameSiteMode represents the SameSite cookie attribute modes.

const (
	// SameSiteDefaultMode sets the 'SameSite' cookie attribute, which is
	// invalid in some older browsers due to changes in the SameSite spec. These
	// browsers will not send the cookie to the server.
	SameSiteDefaultMode SameSiteMode = iota + 1
	SameSiteLaxMode                  // default
	SameSiteStrictMode
	SameSiteNoneMode
)

SameSite options.

Directories

Path Synopsis
Package dex provides an HTTP client for interacting with the Dex OAuth2/OIDC provider to perform the Authorization Code flow.
Package dex provides an HTTP client for interacting with the Dex OAuth2/OIDC provider to perform the Authorization Code flow.
internal
crypto
Package crypto provides an AES-GCM based implementation for frontdex.Crypto.
Package crypto provides an AES-GCM based implementation for frontdex.Crypto.

Jump to

Keyboard shortcuts

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