add initial auth0 module
This commit is contained in:
144
auth/auth0/README.md
Normal file
144
auth/auth0/README.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# Auth0
|
||||||
|
|
||||||
|
A clean, secure, and reusable Auth0 authentication module for Go web applications using **Chi** router.
|
||||||
|
|
||||||
|
This module provides everything you need to add Auth0-based login/logout to your Chi-based application:
|
||||||
|
|
||||||
|
- `/login` — Redirects user to Auth0 Universal Login
|
||||||
|
- `/callback` — Handles Auth0 redirect, verifies ID token, stores user profile & access token in session
|
||||||
|
- `/logout` — Clears session and redirects to Auth0 logout (full single sign-out)
|
||||||
|
- Authentication middleware — Protects routes, redirects unauthenticated users to login
|
||||||
|
- `CurrentUser(r *http.Request)` helper — Retrieve authenticated user claims in handlers
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Secure OAuth2/OIDC flow with state validation and CSRF protection
|
||||||
|
- Clean functional options pattern for dependency injection
|
||||||
|
- Easy-to-use middleware for protected routes
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/derekmwright/go-web/auth/auth0
|
||||||
|
```
|
||||||
|
|
||||||
|
(Replace with your actual repo path when published)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/yourusername/go-auth0-chi"
|
||||||
|
// your session manager, e.g. gorilla/sessions, scollett/chi-sessions, etc.
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
// Your session manager (must implement auth0.SessionManager interface)
|
||||||
|
sessionManager := NewYourSessionManager() // e.g. cookie store
|
||||||
|
|
||||||
|
// Your logger (zap, zerolog, etc. — must implement auth0.Logger)
|
||||||
|
logger := NewYourLogger()
|
||||||
|
|
||||||
|
// Initialize the Auth0 module
|
||||||
|
registerRoutes, requireAuth, err := auth0.New(
|
||||||
|
auth0.WithLogger(logger),
|
||||||
|
auth0.WithSessions(sessionManager),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount the auth routes (usually under root or /auth)
|
||||||
|
registerRoutes(r)
|
||||||
|
|
||||||
|
// Public routes
|
||||||
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("Home page — public"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Protected routes
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(requireAuth) // ← enforces authentication
|
||||||
|
|
||||||
|
r.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := auth0.CurrentUser(r)
|
||||||
|
// user is map[string]any with Auth0 claims (sub, name, email, etc.)
|
||||||
|
w.Write([]byte("Welcome to the dashboard!"))
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Get("/profile", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
profile := auth0.CurrentUser(r).(map[string]any)
|
||||||
|
// Render profile...
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Println("Server starting on :8080")
|
||||||
|
http.ListenAndServe(":8080", r)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routes Added
|
||||||
|
|
||||||
|
When you call `registerRoutes(r)`, the following routes are registered:
|
||||||
|
|
||||||
|
| Route | Method | Purpose |
|
||||||
|
|-------------|--------|---------|
|
||||||
|
| `/login` | GET | Initiates login: generates state, stores in session, redirects to Auth0 |
|
||||||
|
| `/callback` | GET | Auth0 redirect URI: validates state, exchanges code, verifies ID token, stores user & access token in session, redirects to `/` |
|
||||||
|
| `/logout` | GET | Clears session and redirects to Auth0 `/v2/logout` with proper `returnTo` and `client_id` (full SSO logout) |
|
||||||
|
|
||||||
|
You can mount these under a subrouter if preferred:
|
||||||
|
|
||||||
|
```go
|
||||||
|
authRouter := chi.NewRouter()
|
||||||
|
registerRoutes(authRouter)
|
||||||
|
r.Mount("/auth", authRouter) // → /auth/login, /auth/callback, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Required Environment Variables
|
||||||
|
|
||||||
|
The module reads Auth0 configuration from environment variables:
|
||||||
|
|
||||||
|
```env
|
||||||
|
AUTH0_DOMAIN=your-tenant.auth0.com
|
||||||
|
AUTH0_CLIENT_ID=your-client-id
|
||||||
|
AUTH0_CLIENT_SECRET=your-client-secret
|
||||||
|
AUTH0_REDIRECT_URI=http://localhost:8080/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure `AUTH0_REDIRECT_URI` is listed in your Auth0 Application → **Allowed Callback URLs**.
|
||||||
|
|
||||||
|
Also add your post-logout URL (e.g. `http://localhost:8080/`) to **Allowed Logout URLs** in the Auth0 dashboard.
|
||||||
|
|
||||||
|
## Dependencies Injected
|
||||||
|
|
||||||
|
You must provide:
|
||||||
|
|
||||||
|
- `Logger` — with `Debug`, `Info`, `Error` methods (easy to adapt zap, zerolog, log/slog, etc.)
|
||||||
|
- `SessionManager` — with `Get(ctx, key)` and `Put(ctx, key, value)` (compatible with gorilla/sessions, etc.)
|
||||||
|
|
||||||
|
## Session Storage
|
||||||
|
|
||||||
|
The module stores:
|
||||||
|
|
||||||
|
- `"user"` → `map[string]any` with decoded ID token claims (sub, name, email, picture, etc.)
|
||||||
|
- `"access_token"` → raw access token string (useful for calling APIs)
|
||||||
|
|
||||||
|
You can extend this as needed in your own handlers.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The module is designed for easy testing — all dependencies are interfaces. See the `_test.go` files for examples using mocks.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
89
auth/auth0/auth0.go
Normal file
89
auth/auth0/auth0.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package auth0
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
|
"dstar/auth0/authenticator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Debug(msg string, args ...any)
|
||||||
|
Info(msg string, args ...any)
|
||||||
|
Error(msg string, args ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionManager interface {
|
||||||
|
Get(ctx context.Context, key string) any
|
||||||
|
Put(ctx context.Context, key string, value any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Logger Logger
|
||||||
|
Sessions SessionManager
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(deps *Config)
|
||||||
|
|
||||||
|
func WithLogger(l Logger) Option {
|
||||||
|
return func(cfg *Config) {
|
||||||
|
cfg.Logger = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSessions(s SessionManager) Option {
|
||||||
|
return func(cfg *Config) {
|
||||||
|
cfg.Sessions = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type deps struct {
|
||||||
|
auth *authenticator.Authenticator
|
||||||
|
logoutBase *url.URL
|
||||||
|
log Logger
|
||||||
|
sessions SessionManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opts ...Option) (func(chi.Router), Middleware, error) {
|
||||||
|
cfg := Config{}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Logger == nil {
|
||||||
|
return nil, nil, ErrNilLogger
|
||||||
|
}
|
||||||
|
if cfg.Sessions == nil {
|
||||||
|
return nil, nil, ErrNilSessions
|
||||||
|
}
|
||||||
|
|
||||||
|
auth, err := authenticator.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logoutURL, err := url.Parse(auth.LogoutURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("unable to parse logout URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d := &deps{
|
||||||
|
log: cfg.Logger,
|
||||||
|
logoutBase: logoutURL,
|
||||||
|
sessions: cfg.Sessions,
|
||||||
|
auth: auth,
|
||||||
|
}
|
||||||
|
|
||||||
|
mw := func(next http.Handler) http.Handler {
|
||||||
|
return authenticatedMiddleware(d, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(r chi.Router) {
|
||||||
|
Register(r, d)
|
||||||
|
}, mw, nil
|
||||||
|
}
|
||||||
84
auth/auth0/authenticator/authenticator.go
Normal file
84
auth/auth0/authenticator/authenticator.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package authenticator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config defines required configuration values for Auth0.
|
||||||
|
//
|
||||||
|
// * Values are read from the environment.
|
||||||
|
// They cannot be overridden or set from code.
|
||||||
|
type Config struct {
|
||||||
|
Domain string
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
RedirectURI string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Authenticator struct {
|
||||||
|
*oidc.Provider
|
||||||
|
oauth2.Config
|
||||||
|
LogoutURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() (*Authenticator, error) {
|
||||||
|
cfg := Config{
|
||||||
|
Domain: os.Getenv("AUTH0_DOMAIN"),
|
||||||
|
ClientID: os.Getenv("AUTH0_CLIENT_ID"),
|
||||||
|
ClientSecret: os.Getenv("AUTH0_CLIENT_SECRET"),
|
||||||
|
RedirectURI: os.Getenv("AUTH0_REDIRECT_URI"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Domain == "" {
|
||||||
|
return nil, ErrEmptyDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.ClientID == "" {
|
||||||
|
return nil, ErrEmptyClientID
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.ClientSecret == "" {
|
||||||
|
return nil, ErrEmptyClientSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.RedirectURI == "" {
|
||||||
|
return nil, ErrEmptyRedirectURI
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := oidc.NewProvider(
|
||||||
|
context.Background(),
|
||||||
|
"https://"+cfg.Domain+"/",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Authenticator{
|
||||||
|
Provider: provider,
|
||||||
|
Config: oauth2.Config{
|
||||||
|
ClientID: cfg.ClientID,
|
||||||
|
ClientSecret: cfg.ClientSecret,
|
||||||
|
RedirectURL: cfg.RedirectURI,
|
||||||
|
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||||
|
Endpoint: provider.Endpoint(),
|
||||||
|
},
|
||||||
|
LogoutURL: "https://" + cfg.Domain + "/v2/logout",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
|
||||||
|
rawIDToken, ok := token.Extra("id_token").(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrNoIDToken
|
||||||
|
}
|
||||||
|
|
||||||
|
oidcConfig := &oidc.Config{
|
||||||
|
ClientID: a.ClientID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.Verifier(oidcConfig).Verify(ctx, rawIDToken)
|
||||||
|
}
|
||||||
9
auth/auth0/authenticator/errors.go
Normal file
9
auth/auth0/authenticator/errors.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package authenticator
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
var ErrEmptyDomain = fmt.Errorf("domain cannot be empty")
|
||||||
|
var ErrEmptyClientID = fmt.Errorf("client id cannot be empty")
|
||||||
|
var ErrEmptyClientSecret = fmt.Errorf("client secret cannot be empty")
|
||||||
|
var ErrEmptyRedirectURI = fmt.Errorf("redirect uri cannot be empty")
|
||||||
|
var ErrNoIDToken = fmt.Errorf("no id_token field in oauth2 token")
|
||||||
6
auth/auth0/errors.go
Normal file
6
auth/auth0/errors.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package auth0
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrNilLogger = errors.New("logger cannot be nil")
|
||||||
|
var ErrNilSessions = errors.New("sessions cannot be nil")
|
||||||
127
auth/auth0/handlers.go
Normal file
127
auth/auth0/handlers.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package auth0
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const StateKey = "state"
|
||||||
|
|
||||||
|
func generateRandomState() (string, error) {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.RawURLEncoding.EncodeToString(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleLogin(deps *deps) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
state, err := generateRandomState()
|
||||||
|
if err != nil {
|
||||||
|
deps.log.Error("unable to generate random state", "error", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.log.Info("generated state", "state", state)
|
||||||
|
|
||||||
|
if err = deps.sessions.Put(r.Context(), StateKey, state); err != nil {
|
||||||
|
deps.log.Error("unable to store state in session", "error", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, deps.auth.AuthCodeURL(state), http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleLogout(deps *deps) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := deps.sessions.Put(r.Context(), "user", nil); err != nil {
|
||||||
|
deps.log.Error("unable to remove user from session", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := deps.sessions.Put(r.Context(), StateKey, nil); err != nil {
|
||||||
|
deps.log.Error("unable to remove state from session", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := "http"
|
||||||
|
if r.TLS != nil {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
returnTo := scheme + "://" + r.Host
|
||||||
|
|
||||||
|
logout := deps.logoutBase.ResolveReference(&url.URL{})
|
||||||
|
q := logout.Query()
|
||||||
|
q.Add("returnTo", returnTo)
|
||||||
|
q.Add("client_id", deps.auth.ClientID)
|
||||||
|
logout.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
http.Redirect(w, r, logout.String(), http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Authenticator interface {
|
||||||
|
Exchange(context.Context, string) (*oauth2.Token, error)
|
||||||
|
VerifyIDToken(context.Context, *oauth2.Token) (*oidc.IDToken, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleCallback(deps *deps) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, ok := deps.sessions.Get(r.Context(), StateKey).(string)
|
||||||
|
if !ok {
|
||||||
|
deps.log.Error("no state found in session")
|
||||||
|
http.Error(w, "no state found in session", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := deps.sessions.Put(r.Context(), StateKey, nil); err != nil {
|
||||||
|
deps.log.Error("unable to remove state from session", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := deps.auth.Exchange(r.Context(), r.URL.Query().Get("code"))
|
||||||
|
if err != nil {
|
||||||
|
deps.log.Error("unable to exchange auth code for token", "error", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, err := deps.auth.VerifyIDToken(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
deps.log.Error("unable to verify ID token", "error", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var profile map[string]any
|
||||||
|
if err = idToken.Claims(&profile); err != nil {
|
||||||
|
deps.log.Error("unable to decode ID token claims", "error", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = deps.sessions.Put(r.Context(), "user", profile); err != nil {
|
||||||
|
deps.log.Error("unable to store user profile in session", "error", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = deps.sessions.Put(r.Context(), "access_token", token.AccessToken); err != nil {
|
||||||
|
deps.log.Error("unable to store access token in session", "error", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
95
auth/auth0/handlers_test.go
Normal file
95
auth/auth0/handlers_test.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package auth0
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"dstar/auth0/authenticator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockLogger struct{}
|
||||||
|
|
||||||
|
func (m *mockLogger) Debug(msg string, args ...any) {}
|
||||||
|
func (m *mockLogger) Info(msg string, args ...any) {}
|
||||||
|
func (m *mockLogger) Error(msg string, args ...any) {}
|
||||||
|
|
||||||
|
type mockSessionManager struct {
|
||||||
|
store map[string]any
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSessionManager) Get(ctx context.Context, key string) any {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
return m.store[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSessionManager) Put(ctx context.Context, key string, value any) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if value == nil {
|
||||||
|
delete(m.store, key)
|
||||||
|
} else {
|
||||||
|
m.store[key] = value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleLogic(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
existingState any
|
||||||
|
wantRedirect string
|
||||||
|
wantSessionKey string
|
||||||
|
wantSessionValue string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "generates and stores state",
|
||||||
|
wantRedirect: "https://",
|
||||||
|
wantSessionKey: StateKey,
|
||||||
|
wantSessionValue: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
mockSessions := &mockSessionManager{
|
||||||
|
store: make(map[string]any),
|
||||||
|
}
|
||||||
|
if tt.existingState != nil {
|
||||||
|
mockSessions.store[StateKey] = tt.existingState
|
||||||
|
}
|
||||||
|
|
||||||
|
d := &deps{
|
||||||
|
log: &mockLogger{},
|
||||||
|
sessions: mockSessions,
|
||||||
|
auth: &authenticator.Authenticator{
|
||||||
|
Config: oauth2.Config{
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: "https://test.auth0.com/authorize",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("GET", "/login", nil)
|
||||||
|
|
||||||
|
HandleLogin(d)(rr, req)
|
||||||
|
|
||||||
|
if !strings.HasPrefix(rr.Header().Get("Location"), "https://test.auth0.com/authorize") {
|
||||||
|
t.Errorf("wrong redirect: %s", rr.Header().Get("Location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
state := mockSessions.store[StateKey].(string)
|
||||||
|
if len(state) == 0 {
|
||||||
|
t.Fatal("state not stored or empty")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
42
auth/auth0/middleware.go
Normal file
42
auth/auth0/middleware.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package auth0
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userContextKey struct{}
|
||||||
|
|
||||||
|
type Middleware func(http.Handler) http.Handler
|
||||||
|
|
||||||
|
func authenticatedMiddleware(deps *deps, next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
sessionUser := deps.sessions.Get(r.Context(), "user")
|
||||||
|
if sessionUser == nil {
|
||||||
|
state, err := generateRandomState()
|
||||||
|
if err != nil {
|
||||||
|
deps.log.Error("unable to generate random state", "error", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = deps.sessions.Put(r.Context(), StateKey, state); err != nil {
|
||||||
|
deps.log.Error("unable to store state in session", "error", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loginURL := deps.auth.AuthCodeURL(state)
|
||||||
|
http.Redirect(w, r, loginURL, http.StatusFound)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), userContextKey{}, sessionUser)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CurrentUser(r *http.Request) any {
|
||||||
|
return r.Context().Value(userContextKey{})
|
||||||
|
}
|
||||||
11
auth/auth0/routes.go
Normal file
11
auth/auth0/routes.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package auth0
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Register(r chi.Router, deps *deps) {
|
||||||
|
r.Get("/login", HandleLogin(deps))
|
||||||
|
r.Get("/callback", HandleCallback(deps))
|
||||||
|
r.Get("/logout", HandleLogout(deps))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user