189 lines
4.9 KiB
Go
189 lines
4.9 KiB
Go
package site
|
|
|
|
import (
|
|
"context"
|
|
"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:", getIdentity(r.Context()))
|
|
|
|
return response.TemporaryRedirect("/"), nil
|
|
}
|
|
|
|
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) 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 {
|
|
return response.TemporaryRedirect("/"), 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 response.TemporaryRedirect("/"), nil
|
|
}
|
|
|
|
return nil, response.Errorf("failed to authenticate: %w", err)
|
|
}
|
|
|
|
if expiration.Before(time.Now()) {
|
|
return response.TemporaryRedirect("/").Cookie(getExpiredCookie("access_token")), nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func (s *Server) getAccessTokenClaims(r *http.Request) (authentication.AccessTokenClaims, bool) {
|
|
ck, err := r.Cookie("access_token")
|
|
if err != nil {
|
|
return authentication.AccessTokenClaims{}, false
|
|
}
|
|
|
|
ctx := r.Context()
|
|
|
|
claims, expiration, err := s.auth.GetAccessTokenClaimsAndExpiration(ctx, ck.Value)
|
|
if err != nil {
|
|
return authentication.AccessTokenClaims{}, false
|
|
}
|
|
if expiration.Before(time.Now()) {
|
|
return authentication.AccessTokenClaims{}, false
|
|
}
|
|
|
|
return claims, true
|
|
}
|
|
|
|
type identityKey struct{}
|
|
|
|
// stores identity in request context
|
|
func setIdentity(ctx context.Context, id identity) context.Context {
|
|
return context.WithValue(ctx, identityKey{}, id)
|
|
}
|
|
|
|
// 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
|
|
}
|