moved auth into middleware

This commit is contained in:
2026-01-23 23:34:22 -07:00
parent 649595718c
commit e4069e18e2
6 changed files with 103 additions and 129 deletions
+7 -6
View File
@@ -44,18 +44,19 @@ type (
// AccessTokenClaims is the claims Auth0 provides in access tokens // AccessTokenClaims is the claims Auth0 provides in access tokens
AccessTokenClaims struct { AccessTokenClaims struct {
Audience string `json:"aud"` Audience string `json:"aud"` // TODO: fill in from db
Expires int64 `json:"exp"` Expires int64 `json:"exp"`
Expiration time.Time `json:"-"` // parsed Expires
FamilyName string `json:"family_name"` FamilyName string `json:"family_name"`
GivenName string `json:"given_name"` GivenName string `json:"given_name"`
IssuedAt int64 `json:"iat"` IssuedAt int64 `json:"iat"` // TODO: fill in from db
Issuer string `json:"iss"` Issuer string `json:"iss"` // TODO: fill in from db
Name string `json:"name"` Name string `json:"name"`
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
Picture string `json:"picture"` Picture string `json:"picture"`
SessionID string `json:"sid"` SessionID string `json:"sid"` // TODO: fill in from db
Subject string `json:"sub"` Subject string `json:"sub"` // TODO: fill in from db
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"` // TODO: fill in from db
} }
) )
+7 -5
View File
@@ -95,7 +95,7 @@ func (a *Authenticator) DeleteOAuthTokens(ctx context.Context, accessToken strin
return nil return nil
} }
func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, accessToken string) (claims AccessTokenClaims, expiration time.Time, err error) { func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, accessToken string) (claims AccessTokenClaims, err error) {
rows, err := a.db.Query( rows, err := a.db.Query(
ctx, ctx,
` `
@@ -119,7 +119,7 @@ func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, a
}, },
) )
if err != nil { if err != nil {
return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to perform query: %w", err) return AccessTokenClaims{}, fmt.Errorf("failed to perform query: %w", err)
} }
type Row struct { type Row struct {
@@ -135,11 +135,13 @@ func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, a
r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row])
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return AccessTokenClaims{}, time.Time{}, consts.ErrNotFound return AccessTokenClaims{}, consts.ErrNotFound
} }
return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to scan row: %w", err) return AccessTokenClaims{}, fmt.Errorf("failed to scan row: %w", err)
} }
claims.Expires = r.Expiry.Unix()
claims.Expiration = r.Expiry
claims.Name = r.Id_token_custom_claims_name claims.Name = r.Id_token_custom_claims_name
claims.Picture = r.Id_token_custom_claims_picture claims.Picture = r.Id_token_custom_claims_picture
claims.Nickname = r.Id_token_custom_claims_nickname claims.Nickname = r.Id_token_custom_claims_nickname
@@ -147,7 +149,7 @@ func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, a
claims.FamilyName = r.Id_token_custom_claims_family_name claims.FamilyName = r.Id_token_custom_claims_family_name
claims.UpdatedAt = r.Id_token_custom_claims_updated_at claims.UpdatedAt = r.Id_token_custom_claims_updated_at
return claims, r.Expiry, nil return claims, nil
} }
func (a *Authenticator) getRefreshTokenForAccessToken(ctx context.Context, accessToken string) (refreshToken, tokenType string, err error) { func (a *Authenticator) getRefreshTokenForAccessToken(ctx context.Context, accessToken string) (refreshToken, tokenType string, err error) {
+1 -2
View File
@@ -29,7 +29,6 @@ func Routes(
r gin.IRouter, r gin.IRouter,
logger *logging.Logger, logger *logging.Logger,
sq *sse.Queue, sq *sse.Queue,
auth *middleware.Auth,
) { ) {
s := &sseRouter{ s := &sseRouter{
log: logger, log: logger,
@@ -37,7 +36,7 @@ func Routes(
users: make(map[string][maxNumOpenConnectionsPerUser]context.CancelFunc), users: make(map[string][maxNumOpenConnectionsPerUser]context.CancelFunc),
} }
r.GET("/", auth.AuthenticateAndAddIdentityGin(), response.Handler(s.serveEvents)) r.GET("/", response.Handler(s.serveEvents))
} }
func (r *sseRouter) serveEvents(c *gin.Context) (response.Response, error) { func (r *sseRouter) serveEvents(c *gin.Context) (response.Response, error) {
+13 -35
View File
@@ -23,13 +23,12 @@ import (
type ( type (
webpageRouter struct { webpageRouter struct {
log *logging.Logger log *logging.Logger
contentDir string contentDir string
templater *templater.Templater templater *templater.Templater
rawEvents *raw_events.Store rawEvents *raw_events.Store
accts *accounts.Store accts *accounts.Store
etsy *etsy_platform.Platform etsy *etsy_platform.Platform
authMiddleware *middleware.Auth
} }
// ErrTemplateNotFound is returned if the reason the template failed to compile // ErrTemplateNotFound is returned if the reason the template failed to compile
@@ -41,12 +40,12 @@ type (
func SetupRoutes( func SetupRoutes(
logger *logging.Logger, logger *logging.Logger,
r gin.IRouter, r gin.IRoutes,
contentDir string, contentDir string,
rawEvents *raw_events.Store, rawEvents *raw_events.Store,
accts *accounts.Store, accts *accounts.Store,
etsy *etsy_platform.Platform, etsy *etsy_platform.Platform,
authMiddleware *middleware.Auth, auth *middleware.Auth,
) { ) {
s := &webpageRouter{ s := &webpageRouter{
@@ -90,34 +89,13 @@ func SetupRoutes(
} }
}, },
}), }),
rawEvents: rawEvents, rawEvents: rawEvents,
accts: accts, accts: accts,
etsy: etsy, etsy: etsy,
authMiddleware: authMiddleware,
} }
authenticate := s.authMiddleware.AuthenticateAndAddIdentityToRequest() r.GET("", response.Handler(s.serveTemplate))
r.GET("/*rest", response.Handler(auth.Authenticate()), response.Handler(s.serveTemplate))
r.GET("/*rest", response.Handler(func(c *gin.Context) (response.Response, error) {
c.Request.URL.Path = c.Request.URL.Path[3:]
defer func() {
c.Request.URL.Path = "/ui" + c.Request.URL.Path
}()
p := c.Request.URL.Path
if p == "/" || p == "" {
c, err := s.authMiddleware.AddIdentityToRequest(c)
if err != nil {
return nil, err
}
return s.serveTemplate(c)
}
if res, err := authenticate(c); res != nil || err != nil {
return res, err
}
return s.serveTemplate(c)
}))
} }
// GET / // GET /
+45 -77
View File
@@ -52,91 +52,69 @@ func NewAuth(
} }
} }
func (a *Auth) AddIdentity(fn response.HandlerFunc) response.HandlerFunc { // AddIdentityToRequest will add an Identity to the context that can then be retrieved via GetIdentity.
return func(c *gin.Context) (response.Response, error) { func (a *Auth) AddIdentityToRequest(c *gin.Context) {
c, err := a.AddIdentityToRequest(c) if err := a.addIdentityToRequest(c); err != nil {
if err != nil { c.Error(err)
return nil, err c.Abort()
}
return fn(c)
} }
} }
func (a *Auth) AddIdentityToRequest(c *gin.Context) (*gin.Context, error) { func (a *Auth) addIdentityToRequest(c *gin.Context) error {
r := c.Request r := c.Request
ck, err := r.Cookie("access_token") ck, err := r.Cookie("access_token")
if err != nil { if err != nil {
return c, nil return nil
} }
ctx := r.Context() ctx := r.Context()
accessToken := ck.Value accessToken := ck.Value
claims, expiration, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken) claims, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
if err != nil { if err != nil {
if errors.Is(err, consts.ErrNotFound) { if errors.Is(err, consts.ErrNotFound) {
return c, nil return nil
} }
return c, response.Errorf("failed to load authentication details: %w", err) return response.Errorf("failed to load authentication details: %w", err)
} }
expiration := claims.Expiration
if expiration.Before(time.Now()) { if expiration.Before(time.Now()) {
return c, nil return nil
} }
user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken) user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken)
if err != nil { if err != nil {
return c, response.Errorf("failed to load user and account defails: %w", err) return response.Errorf("failed to load user and account defails: %w", err)
} }
c.Request = r.WithContext(SetIdentity(ctx, Identity{ c.Request = r.WithContext(SetIdentity(c, Identity{
AccessToken: accessToken, AccessToken: accessToken,
Claims: claims, Claims: claims,
User: user, User: user,
Account: acct, Account: acct,
})) }))
return c, nil return nil
} }
// TODO: use the new middleware pattern // Authenticate should only be used along with and after AddIdentityToRequest
// auth middleware to verify access_token cookie and set custom claims in the request context // Typically used with response.Handler to make a gin.HandlerFunc.
func (a *Auth) AuthenticateAndAddIdentityGin(assertions ...AuthorizationAssertions) gin.HandlerFunc { func (a *Auth) Authenticate(assertions ...AuthorizationAssertions) func(c *gin.Context) (response.Response, error) {
fn := a.AuthenticateAndAddIdentityToRequest(assertions...)
return response.Handler(fn)
}
func (a *Auth) AuthenticateAndAddIdentityToRequest(assertions ...AuthorizationAssertions) response.HandlerFunc {
return func(c *gin.Context) (response.Response, error) { return func(c *gin.Context) (response.Response, error) {
r := c.Request id, ok := getIdentity(c)
ck, err := r.Cookie("access_token") if !ok {
if err != nil { return nil, response.Unauthorized().
return response.TemporaryRedirect("/"). HTML([]byte(`
JSON("no access_token cookie provided"), nil <h1>Unauthorized</h1>
} <a href="/">Return to app</a>
`)) // TODO: would be nice to have a better page for this
ctx := r.Context()
accessToken := ck.Value
claims, expiration, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
if err != nil {
if errors.Is(err, consts.ErrNotFound) {
u, err := a.newLoginURL(ctx, a.auth, r.URL.String())
if err != nil {
return nil, response.Errorf("failed to generate login url: %w", err)
}
return response.TemporaryRedirect(u), nil
}
return nil, response.Errorf("failed to authenticate: %w", err)
} }
expiration := id.Claims.Expiration
now := time.Now() now := time.Now()
// refresh tokens, when the access token is "old enough" // refresh tokens, when the access token is "old enough"
@@ -144,7 +122,7 @@ func (a *Auth) AuthenticateAndAddIdentityToRequest(assertions ...AuthorizationAs
// id token lifetime is 48 hours, allowing a person to use the app everyday comfortably, with wiggle room, without having to log in. // id token lifetime is 48 hours, allowing a person to use the app everyday comfortably, with wiggle room, without having to log in.
const idTokenLifetime = 48 * time.Hour const idTokenLifetime = 48 * time.Hour
if refreshFloor := expiration.Add(-(idTokenLifetime / 4)); refreshFloor.Before(now) { if refreshFloor := expiration.Add(-(idTokenLifetime / 4)); refreshFloor.Before(now) {
accessToken, expiration, err = a.auth.RefreshAccessToken(ctx, accessToken) accessToken, expiration, err := a.auth.RefreshAccessToken(c, id.AccessToken)
if err != nil { if err != nil {
a.log.Warn("failed to refresh access token", "error", err) a.log.Warn("failed to refresh access token", "error", err)
return response.TemporaryRedirect("/"). return response.TemporaryRedirect("/").
@@ -153,65 +131,55 @@ func (a *Auth) AuthenticateAndAddIdentityToRequest(assertions ...AuthorizationAs
} }
// 'redirect' to same url, to set the new access_token cookie // 'redirect' to same url, to set the new access_token cookie
return response.TemporaryRedirect(r.URL.String()). return response.TemporaryRedirect(c.Request.URL.String()).
Cookie(cookies.AccessToken(accessToken, expiration)), nil Cookie(cookies.AccessToken(accessToken, expiration)), nil
} }
// add identity info to request context
user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken)
if err != nil {
return nil, response.Errorf("failed to authorize: %w", err)
}
for _, as := range assertions { for _, as := range assertions {
res, err := as(c) if res, err := as(c); res != nil || err != nil {
if res != nil || err != nil {
return res, err return res, err
} }
} }
id := Identity{
AccessToken: accessToken,
Claims: claims,
User: user,
Account: acct,
}
c.Set(identityKeyString, id)
c.Request = r.WithContext(SetIdentity(ctx, id))
return nil, nil return nil, nil
} }
} }
type identityKey struct{} type identityKey struct{}
const identityKeyString = "identity"
// stores identity in context // stores identity in context
func SetIdentity(ctx context.Context, id Identity) context.Context { func SetIdentity(ctx context.Context, id Identity) context.Context {
c, ok := ctx.(*gin.Context)
if ok {
c.Set(identityKey{}, id)
ctx = c.Request.Context()
}
return context.WithValue(ctx, identityKey{}, id) return context.WithValue(ctx, identityKey{}, id)
} }
// get identity from context // get identity from context
func GetIdentity(ctx context.Context) Identity { func GetIdentity(ctx context.Context) Identity {
id, _ := getIdentity(ctx)
return id
}
func getIdentity(ctx context.Context) (Identity, bool) {
id, ok := ctx.Value(identityKey{}).(Identity) id, ok := ctx.Value(identityKey{}).(Identity)
if ok { if ok {
return id return id, true
} }
c, ok := ctx.(*gin.Context) c, ok := ctx.(*gin.Context)
if !ok { if !ok {
return Identity{} return Identity{}, false
} }
v, ok := c.Get(identityKeyString) v, ok := c.Get(identityKey{})
if !ok { if !ok {
return Identity{} return Identity{}, false
} }
id, _ = v.(Identity) id, ok = v.(Identity)
return id return id, ok
} }
+30 -4
View File
@@ -79,7 +79,11 @@ func NewRouter(
// kind of a dumb way to capture routes for webpages // kind of a dumb way to capture routes for webpages
templates_api.SetupRoutes( templates_api.SetupRoutes(
logger.WithGroup("templates"), logger.WithGroup("templates"),
r.Group("/ui"), r.Group(
"/ui",
stripPrefix("/ui"),
authMiddleware.AddIdentityToRequest,
),
contentDir, contentDir,
rawEvents, rawEvents,
accts, accts,
@@ -107,13 +111,20 @@ func NewRouter(
auth, auth,
) )
sse_api.Routes( sse_api.Routes(
api.Group("/events"), api.Group(
"/events",
authMiddleware.AddIdentityToRequest,
response.Handler(authMiddleware.Authenticate()),
),
apiLogger.WithGroup("/events"), apiLogger.WithGroup("/events"),
sq, sq,
authMiddleware,
) )
accounts_api.Routes( accounts_api.Routes(
api.Group("/accounts", authMiddleware.AuthenticateAndAddIdentityGin()), api.Group(
"/accounts",
authMiddleware.AddIdentityToRequest,
response.Handler(authMiddleware.Authenticate()),
),
apiLogger.WithGroup("/accounts"), apiLogger.WithGroup("/accounts"),
accts, accts,
unp.Group("/accounts"), unp.Group("/accounts"),
@@ -158,3 +169,18 @@ func fileServer(urlPrefix, dir string, beforeServe func(c *gin.Context)) gin.Han
// TODO: need a c.Next()? // TODO: need a c.Next()?
} }
} }
func stripPrefix(prefix string) gin.HandlerFunc {
return func(c *gin.Context) {
if !strings.HasPrefix(c.Request.URL.Path, prefix) {
return
}
c.Request.URL.Path = c.Request.URL.Path[len(prefix):]
defer func() {
c.Request.URL.Path = prefix + c.Request.URL.Path
}()
c.Next()
}
}