9 Commits

Author SHA1 Message Date
miloschwartz
cc45f414de update license copyright 2025-11-01 12:51:50 -07:00
miloschwartz
14df42f0ab strip headers, change display name, add icon 2025-11-01 12:50:15 -07:00
miloschwartz
cab2424e6e add templates 2025-09-29 16:40:42 -07:00
Milo Schwartz
d553b6ca77 Merge pull request #7 from pyrho/feat/auth-headers
feat: add auth headers to request
2025-06-04 12:35:46 -04:00
Damien Rajon
0baba997d7 feat: add auth headers to request
These changes are needed for the related changes in pangolin to work.
See https://github.com/fosrl/pangolin/pull/807#event-17985130896
2025-06-04 18:30:25 +02:00
miloschwartz
e951e42b4d remove explicit access token check and pass query params and headers in verify session 2025-04-06 11:24:47 -04:00
miloschwartz
3a180988be add .go-version 2025-03-03 22:34:59 -05:00
Milo Schwartz
9cae64dac9 pass client ip to pangolin 2025-01-27 22:38:01 -05:00
Milo Schwartz
002997b2a0 refactor auth to use exchange token system 2025-01-26 17:23:06 -05:00
10 changed files with 252 additions and 39 deletions

BIN
.assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,47 @@
body:
- type: textarea
attributes:
label: Summary
description: A clear and concise summary of the requested feature.
validations:
required: true
- type: textarea
attributes:
label: Motivation
description: |
Why is this feature important?
Explain the problem this feature would solve or what use case it would enable.
validations:
required: true
- type: textarea
attributes:
label: Proposed Solution
description: |
How would you like to see this feature implemented?
Provide as much detail as possible about the desired behavior, configuration, or changes.
validations:
required: true
- type: textarea
attributes:
label: Alternatives Considered
description: Describe any alternative solutions or workarounds you've thought about.
validations:
required: false
- type: textarea
attributes:
label: Additional Context
description: Add any other context, mockups, or screenshots about the feature request here.
validations:
required: false
- type: markdown
attributes:
value: |
Before submitting, please:
- Check if there is an existing issue for this feature.
- Clearly explain the benefit and use case.
- Be as specific as possible to help contributors evaluate and implement.

51
.github/ISSUE_TEMPLATE/1.bug_report.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: Bug Report
description: Create a bug report
labels: []
body:
- type: textarea
attributes:
label: Describe the Bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: Environment
description: Please fill out the relevant details below for your environment.
value: |
- OS Type & Version: (e.g., Ubuntu 22.04)
- Pangolin Version:
- Gerbil Version:
- Traefik Version:
- Newt Version:
- Olm Version: (if applicable)
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: |
Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below.
If using code blocks, make sure syntax highlighting is correct and double-check that the rendered preview is not broken.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: markdown
attributes:
value: |
Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear.
- type: markdown
attributes:
value: |
Contributors should be able to follow the steps provided in order to reproduce the bug.

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Need help or have questions?
url: https://github.com/orgs/fosrl/discussions
about: Ask questions, get help, and discuss with other community members
- name: Request a Feature
url: https://github.com/orgs/fosrl/discussions/new?category=feature-requests
about: Feature requests should be opened as discussions so others can upvote and comment

1
.gitignore vendored
View File

@@ -1 +1,2 @@
go.sum
.DS_Store

1
.go-version Normal file
View File

@@ -0,0 +1 @@
1.23.2

View File

@@ -1,12 +1,12 @@
displayName: Fossorial Badger
displayName: Pangolin (Badger)
type: middleware
iconPath: .assets/icon.png
import: github.com/fosrl/badger
summary: Middleware auth bouncer for Pangolin
testData:
apiBaseUrl: http://localhost:3001/api/v1
userSessionCookieName: p_session
resourceSessionCookieName: p_resource_session
accessTokenQueryParam: p_token
apiBaseUrl: "http://localhost:3001/api/v1"
userSessionCookieName: "p_session_token"
resourceSessionRequestParam: "p_session_request"

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 Fossorial LLC
Copyright (c) 2025 Fossorial Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -16,9 +16,8 @@ Badger requires the following configuration parameters to be specified in your [
```yaml
apiBaseUrl: "http://localhost:3001/api/v1"
userSessionCookieName: "p_session"
resourceSessionCookieName: "p_resource_session"
accessTokenQueryParam: "p_token"
userSessionCookieName: "p_session_token"
resourceSessionRequestParam: "p_session_request"
```
## License

166
main.go
View File

@@ -10,10 +10,17 @@ import (
)
type Config struct {
APIBaseUrl string `json:"apiBaseUrl"`
UserSessionCookieName string `json:"userSessionCookieName"`
ResourceSessionCookieName string `json:"resourceSessionCookieName"`
AccessTokenQueryParam string `json:"accessTokenQueryParam"`
APIBaseUrl string `json:"apiBaseUrl"`
UserSessionCookieName string `json:"userSessionCookieName"`
ResourceSessionRequestParam string `json:"resourceSessionRequestParam"`
}
type Badger struct {
next http.Handler
name string
apiBaseUrl string
userSessionCookieName string
resourceSessionRequestParam string
}
type VerifyBody struct {
@@ -23,24 +30,35 @@ type VerifyBody struct {
RequestHost *string `json:"host"`
RequestPath *string `json:"path"`
RequestMethod *string `json:"method"`
AccessToken *string `json:"accessToken,omitempty"`
TLS bool `json:"tls"`
RequestIP *string `json:"requestIp,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Query map[string]string `json:"query,omitempty"`
}
type VerifyResponse struct {
Data struct {
Valid bool `json:"valid"`
RedirectURL *string `json:"redirectUrl"`
Valid bool `json:"valid"`
RedirectURL *string `json:"redirectUrl"`
Username *string `json:"username,omitempty"`
Email *string `json:"email,omitempty"`
Name *string `json:"name,omitempty"`
ResponseHeaders map[string]string `json:"responseHeaders,omitempty"`
} `json:"data"`
}
type Badger struct {
next http.Handler
name string
apiBaseUrl string
userSessionCookieName string
resourceSessionCookieName string
accessTokenQueryParam string
type ExchangeSessionBody struct {
RequestToken *string `json:"requestToken"`
RequestHost *string `json:"host"`
RequestIP *string `json:"requestIp,omitempty"`
}
type ExchangeSessionResponse struct {
Data struct {
Valid bool `json:"valid"`
Cookie *string `json:"cookie"`
ResponseHeaders map[string]string `json:"responseHeaders,omitempty"`
} `json:"data"`
}
func CreateConfig() *Config {
@@ -49,23 +67,67 @@ func CreateConfig() *Config {
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
return &Badger{
next: next,
name: name,
apiBaseUrl: config.APIBaseUrl,
userSessionCookieName: config.UserSessionCookieName,
resourceSessionCookieName: config.ResourceSessionCookieName,
accessTokenQueryParam: config.AccessTokenQueryParam,
next: next,
name: name,
apiBaseUrl: config.APIBaseUrl,
userSessionCookieName: config.UserSessionCookieName,
resourceSessionRequestParam: config.ResourceSessionRequestParam,
}, nil
}
func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
cookies := p.extractCookies(req)
var accessToken *string
queryValues := req.URL.Query()
if token := queryValues.Get(p.accessTokenQueryParam); token != "" {
accessToken = &token
queryValues.Del(p.accessTokenQueryParam)
if sessionRequestValue := queryValues.Get(p.resourceSessionRequestParam); sessionRequestValue != "" {
body := ExchangeSessionBody{
RequestToken: &sessionRequestValue,
RequestHost: &req.Host,
RequestIP: &req.RemoteAddr,
}
jsonData, err := json.Marshal(body)
if err != nil {
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
return
}
verifyURL := fmt.Sprintf("%s/badger/exchange-session", p.apiBaseUrl)
resp, err := http.Post(verifyURL, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
var result ExchangeSessionResponse
err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
return
}
if result.Data.Cookie != nil && *result.Data.Cookie != "" {
rw.Header().Add("Set-Cookie", *result.Data.Cookie)
queryValues.Del(p.resourceSessionRequestParam)
cleanedQuery := queryValues.Encode()
originalRequestURL := fmt.Sprintf("%s://%s%s", p.getScheme(req), req.Host, req.URL.Path)
if cleanedQuery != "" {
originalRequestURL = fmt.Sprintf("%s?%s", originalRequestURL, cleanedQuery)
}
if result.Data.ResponseHeaders != nil {
for key, value := range result.Data.ResponseHeaders {
rw.Header().Add(key, value)
}
}
fmt.Println("Got exchange token, redirecting to", originalRequestURL)
http.Redirect(rw, req, originalRequestURL, http.StatusFound)
return
}
}
cleanedQuery := queryValues.Encode()
@@ -76,6 +138,20 @@ func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
verifyURL := fmt.Sprintf("%s/badger/verify-session", p.apiBaseUrl)
headers := make(map[string]string)
for name, values := range req.Header {
if len(values) > 0 {
headers[name] = values[0] // Send only the first value for simplicity
}
}
queryParams := make(map[string]string)
for key, values := range queryValues {
if len(values) > 0 {
queryParams[key] = values[0]
}
}
cookieData := VerifyBody{
Sessions: cookies,
OriginalRequestURL: originalRequestURL,
@@ -83,8 +159,10 @@ func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
RequestHost: &req.Host,
RequestPath: &req.URL.Path,
RequestMethod: &req.Method,
AccessToken: accessToken,
TLS: req.TLS != nil,
RequestIP: &req.RemoteAddr,
Headers: headers,
Query: queryParams,
}
jsonData, err := json.Marshal(cookieData)
@@ -100,7 +178,6 @@ func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
defer resp.Body.Close()
// pass through cookies
for _, setCookie := range resp.Header["Set-Cookie"] {
rw.Header().Add("Set-Cookie", setCookie)
}
@@ -117,24 +194,53 @@ func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return
}
req.Header.Del("Remote-User")
req.Header.Del("Remote-Email")
req.Header.Del("Remote-Name")
if result.Data.ResponseHeaders != nil {
for key, value := range result.Data.ResponseHeaders {
rw.Header().Add(key, value)
}
}
if result.Data.RedirectURL != nil && *result.Data.RedirectURL != "" {
fmt.Println("Badger: Redirecting to", *result.Data.RedirectURL)
http.Redirect(rw, req, *result.Data.RedirectURL, http.StatusFound)
return
}
if !result.Data.Valid {
http.Error(rw, "Unauthorized", http.StatusUnauthorized)
if result.Data.Valid {
if result.Data.Username != nil {
req.Header.Add("Remote-User", *result.Data.Username)
}
if result.Data.Email != nil {
req.Header.Add("Remote-Email", *result.Data.Email)
}
if result.Data.Name != nil {
req.Header.Add("Remote-Name", *result.Data.Name)
}
fmt.Println("Badger: Valid session")
p.next.ServeHTTP(rw, req)
return
}
p.next.ServeHTTP(rw, req)
http.Error(rw, "Unauthorized", http.StatusUnauthorized)
}
func (p *Badger) extractCookies(req *http.Request) map[string]string {
cookies := make(map[string]string)
isSecureRequest := req.TLS != nil
for _, cookie := range req.Cookies() {
if strings.HasPrefix(cookie.Name, p.userSessionCookieName) || strings.HasPrefix(cookie.Name, p.resourceSessionCookieName) {
if strings.HasPrefix(cookie.Name, p.userSessionCookieName) {
if cookie.Secure && !isSecureRequest {
continue
}
cookies[cookie.Name] = cookie.Value
}
}