authorization enforced on webpages

This commit is contained in:
2025-12-30 18:08:22 -07:00
parent c01d700482
commit 240d82344c
24 changed files with 509 additions and 123 deletions
+121 -12
View File
@@ -5,24 +5,82 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"ruben/inventory2/internal/consts"
"ruben/inventory2/internal/domains/accounts"
"ruben/inventory2/internal/domains/authentication"
"ruben/inventory2/internal/site/response"
)
// just keep this around long enough for testing auth middleware..
func (s *Server) testAuthEndpoint(r *http.Request) (response.Response, error) {
fmt.Println("AUTH TEST SUCCESS:", getAccessTokenClaims(r.Context()))
fmt.Println("AUTH TEST SUCCESS:", getIdentity(r.Context()))
return response.TemporaryRedirect("/"), nil
}
type customClaimsKey struct{}
type identity struct {
AccessToken string
Claims authentication.AccessTokenClaims
User accounts.OAuthUser
Account *accounts.Account
}
func (s *Server) addIdentity(fn response.HandlerFunc) response.HandlerFunc {
return func(r *http.Request) (response.Response, error) {
r, err := s.addIdentityToRequest(r)
if err != nil {
return nil, err
}
return fn(r)
}
}
func (s *Server) addIdentityToRequest(r *http.Request) (*http.Request, error) {
ck, err := r.Cookie("access_token")
if err != nil {
return r, nil
}
ctx := r.Context()
accessToken := ck.Value
claims, expiration, err := s.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
if err != nil {
if errors.Is(err, consts.ErrNotFound) {
return r, nil
}
return r, response.Errorf("failed to load authentication details: %w", err)
}
if expiration.Before(time.Now()) {
return r, nil
}
user, acct, err := s.accts.GetUserAndAccountByAccessToken(ctx, accessToken)
if err != nil {
return r, response.Errorf("failed to load user and account defails: %w", err)
}
return r.WithContext(setIdentity(ctx, identity{
AccessToken: accessToken,
Claims: claims,
User: user,
Account: acct,
})), nil
}
// TODO: after getting auth, consider making http handler functions take 'claims', etc, as function arguments
// - then consider doing the same with authorizationAssertions
// auth middleware to verify access_token cookie and set custom claims in the request context
func (s *Server) authenticate(f response.HandlerFunc) response.HandlerFunc {
func (s *Server) authenticateAndAddIdentity(f response.HandlerFunc, assertions ...authorizationAssertions) response.HandlerFunc {
return func(r *http.Request) (response.Response, error) {
ck, err := r.Cookie("access_token")
if err != nil {
@@ -31,7 +89,9 @@ func (s *Server) authenticate(f response.HandlerFunc) response.HandlerFunc {
ctx := r.Context()
claims, expiration, err := s.auth.GetAccessTokenClaimsAndExpiration(ctx, ck.Value)
accessToken := ck.Value
claims, expiration, err := s.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
if err != nil {
if errors.Is(err, consts.ErrNotFound) {
return response.TemporaryRedirect("/"), nil
@@ -44,7 +104,32 @@ func (s *Server) authenticate(f response.HandlerFunc) response.HandlerFunc {
return response.TemporaryRedirect("/").Cookie(getExpiredCookie("access_token")), nil
}
return f(r.WithContext(setAccessTokenClaims(ctx, claims)))
user, acct, err := s.accts.GetUserAndAccountByAccessToken(ctx, accessToken)
if err != nil {
return nil, response.Errorf("failed to authorize: %w", err)
}
for _, as := range assertions {
if res, err := as(r); res != nil || err != nil {
return res, err
}
}
return f(r.WithContext(setIdentity(ctx, identity{
AccessToken: accessToken,
Claims: claims,
User: user,
Account: acct,
})))
}
}
type authorizationAssertions = response.HandlerFunc
// TODO: test this!
func authorizeByMatchingAccountID_tmp(acctIDPathPosition int) authorizationAssertions {
return func(r *http.Request) (response.Response, error) {
return nil, authorizeByMatchingAccountID(r, acctIDPathPosition)
}
}
@@ -67,13 +152,37 @@ func (s *Server) getAccessTokenClaims(r *http.Request) (authentication.AccessTok
return claims, true
}
// stores custom claims in request context
func setAccessTokenClaims(ctx context.Context, claims authentication.AccessTokenClaims) context.Context {
return context.WithValue(ctx, customClaimsKey{}, claims)
type identityKey struct{}
// stores identity in request context
func setIdentity(ctx context.Context, id identity) context.Context {
return context.WithValue(ctx, identityKey{}, id)
}
// get custom claims from request context
func getAccessTokenClaims(ctx context.Context) authentication.AccessTokenClaims {
c, _ := ctx.Value(customClaimsKey{}).(authentication.AccessTokenClaims)
return c
// get identity from request context
func getIdentity(ctx context.Context) identity {
id, _ := ctx.Value(identityKey{}).(identity)
return id
}
func authorizeByMatchingAccountID(r *http.Request, acctIDPathPosition int) error {
pathParts := strings.Split(strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/"), "/"), "/")
if len(pathParts) < acctIDPathPosition {
return fmt.Errorf("authorization failed due to unexpected path: %s", r.URL.Path)
}
part := pathParts[acctIDPathPosition-1]
acctID, err := strconv.ParseInt(part, 10, 64)
if err != nil {
return response.NotFound().
Msgf("account does not exist: %s", part)
}
id := getIdentity(r.Context())
if id.Account == nil || id.Account.ID != acctID {
return response.Unauthorized().
Msgf("user does not have access to account %d", acctID)
}
return nil
}