Skip to content

Commit

Permalink
add HSP IAM audit capabilities
Browse files Browse the repository at this point in the history
  • Loading branch information
EriksonBahr committed Jul 1, 2024
1 parent f7de8bb commit 85b4dca
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 1 deletion.
20 changes: 19 additions & 1 deletion oauthproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/redirect"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/audit"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
Expand Down Expand Up @@ -104,6 +105,8 @@ type OAuthProxy struct {
serveMux *mux.Router
redirectValidator redirect.Validator
appDirector redirect.AppDirector

AuditClient *audit.AuditClient
}

// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
Expand Down Expand Up @@ -202,6 +205,17 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
Validator: redirectValidator,
})

auditClient, err := audit.NewAuditClient(&audit.AuditClientOpts{
Url: opts.AuditUrl,
Enabled: opts.EnableAudit,
ProductName: opts.AuditProductName,
ProductKey: opts.AuditProductKey,
SharedKey: opts.AuditSharedKey,
SecretKey: opts.AuditSecretKey})
if err != nil {
return nil, fmt.Errorf("error setting up server (audit client): %v", err)
}

p := &OAuthProxy{
CookieOptions: &opts.Cookie,
Validator: validator,
Expand Down Expand Up @@ -231,6 +245,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
upstreamProxy: upstreamProxy,
redirectValidator: redirectValidator,
appDirector: appDirector,
AuditClient: auditClient,
}
p.buildServeMux(opts.ProxyPrefix)

Expand Down Expand Up @@ -851,7 +866,9 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
}

if !csrf.CheckOAuthState(nonce) {
logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
errorMsg := "Invalid authentication via OAuth2: CSRF token mismatch, potential attack"
logger.PrintAuthf(session.Email, req, logger.AuthFailure, errorMsg)
p.AuditClient.CreateFailedLoginAuditEntry(session, appRedirect, req.Header.Get("edisp-org-id"), errorMsg)
p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.")
return
}
Expand Down Expand Up @@ -880,6 +897,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
return
}
p.AuditClient.CreateSuccessfulLoginAuditEntry(session, appRedirect, req.Header.Get("edisp-org-id"))
http.Redirect(rw, req, appRedirect, http.StatusFound)
} else {
logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
Expand Down
16 changes: 16 additions & 0 deletions pkg/apis/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ type Options struct {
oidcVerifier internaloidc.IDTokenVerifier
jwtBearerVerifiers []internaloidc.IDTokenVerifier
realClientIPParser ipapi.RealClientIPParser

// Philips opts
EnableAudit bool `flag:"enable-audit" cfg:"enable_audit"`
AuditUrl string `flag:"audit-url" cfg:"audit_url"`

Check failure on line 78 in pkg/apis/options/options.go

View workflow job for this annotation

GitHub Actions / Lint - golangci-lint

struct field `AuditUrl` should be `AuditURL` (golint)
AuditProductName string `flag:"audit-product-name" cfg:"audit_product_name"`
AuditProductKey string `flag:"audit-product-key" cfg:"audit_product_key"`
AuditSharedKey string `flag:"audit-shared-key" cfg:"audit_shared_key"`
AuditSecretKey string `flag:"audit-secret-key" cfg:"audit_secret_key"`
}

// Options for Getting internal values
Expand Down Expand Up @@ -149,6 +157,14 @@ func NewFlagSet() *pflag.FlagSet {
flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)")
flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints")

// Philips opts
flagSet.Bool("enable-audit", false, "Persist audit entries in the audit server upon user authentication and relevant errors")
flagSet.String("audit-url", "", "The url where the audit entries will be posted")
flagSet.String("audit-product-name", "", "The name of the product to be used in the audit")
flagSet.String("audit-product-key", "", "The key of the audit")
flagSet.String("audit-shared-key", "", "The shared key of the audit")
flagSet.String("audit-secret-key", "", "The secret key of the audit")

flagSet.AddFlagSet(cookieFlagSet())
flagSet.AddFlagSet(loggingFlagSet())
flagSet.AddFlagSet(templatesFlagSet())
Expand Down
160 changes: 160 additions & 0 deletions pkg/audit/audit_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package audit

import (
"encoding/json"
"errors"
"fmt"
"log"
"strings"
"time"

"github.com/go-resty/resty/v2"

Check failure on line 11 in pkg/audit/audit_client.go

View workflow job for this annotation

GitHub Actions / Tests - Executing unit tests

no required module provides package github.com/go-resty/resty/v2; to add it:
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
)

type AuditClientOpts struct {
Enabled bool
Url string
ProductKey string
ProductName string
SharedKey string
SecretKey string
}

// AuditClient interface for communicating with audit system
type AuditClient struct {
enabled bool
apiSignature APISignature
opts *AuditClientOpts
client *resty.Client

Check failure on line 30 in pkg/audit/audit_client.go

View workflow job for this annotation

GitHub Actions / Lint - golangci-lint

undefined: resty (typecheck)
}

func NewAuditClient(opts *AuditClientOpts) (*AuditClient, error) {
if opts.Enabled {
log.Print("Audit entries will be created since OAUTH2_PROXY_ENABLE_AUDIT is true")
err := opts.Validate()
if err != nil {
return nil, err
}
} else {
log.Print("Audit entries will NOT be created since OAUTH2_PROXY_ENABLE_AUDIT is false")
}
apiSignature := NewAPISignature(opts.SecretKey, opts.SharedKey)
client := resty.New()

Check failure on line 44 in pkg/audit/audit_client.go

View workflow job for this annotation

GitHub Actions / Lint - golangci-lint

undefined: resty (typecheck)
client.SetRetryCount(3).
SetRetryWaitTime(5 * time.Second).
SetRetryMaxWaitTime(20 * time.Second).
SetContentLength(true).
SetRetryAfter(func(client *resty.Client, resp *resty.Response) (time.Duration, error) {

Check failure on line 49 in pkg/audit/audit_client.go

View workflow job for this annotation

GitHub Actions / Lint - golangci-lint

undefined: resty (typecheck)
return 0, fmt.Errorf("%w: retry quota exceeded", ErrPersitAuditEvent)
})
return &AuditClient{enabled: opts.Enabled, apiSignature: apiSignature, client: client, opts: opts}, nil
}

func (a *AuditClient) CreateSuccessfulLoginAuditEntry(ss *sessions.SessionState, appUrl string, tenantId string) {
a.createAuditEntry(ss, appUrl, tenantId, "0", "Success")
}

func (a *AuditClient) CreateFailedLoginAuditEntry(ss *sessions.SessionState, appUrl string, tenantId string, errorDesc string) {
a.createAuditEntry(ss, appUrl, tenantId, "1", errorDesc)
}

func (a *AuditClient) createAuditEntry(ss *sessions.SessionState, appUrl string, tenantId string, outcomeCode string, outcomeDesc string) {
if !a.enabled {
return
}
auditObject := AuditEvent{
ResourceType: "AuditEvent",
Event: &Event{
Type: &Coding{
System: "http://hl7.org/fhir/ValueSet/audit-event-type", Version: "1", Code: "110114", Display: "User Authentication"},
Action: "E",
DateTime: time.Now().UTC().Format(time.RFC3339),
Outcome: outcomeCode,
OutcomeDesc: outcomeDesc},

Participant: []*Participant{
{AltID: ss.User, UserID: UserID{Value: ss.Email}, Name: ss.PreferredUsername, Requestor: true}},
Source: Source{
Identifier: Identifier{
Type: &Coding{
System: "http://hl7.org/fhir/ValueSet/identifier-type",
Code: "4",
Display: "Application Server",
},
Value: ss.Email,
},
Type: []*Coding{{System: "http://hl7.org/fhir/security-source-type", Code: "1", Display: "End-user display device, diagnostic device."}},
Extension: []*Extension{
{
URL: appUrl,
Extension: []*ExtensionContent{
{
URL: "applicationName",
ValueString: a.opts.ProductName,
},
{
URL: "applicationVersion",
ValueString: "1",
},
{
URL: "serverName",
ValueString: "oauth2proxy",
},
{
URL: "componentName",
ValueString: "oauth2proxy",
},
{
URL: "productKey",
ValueString: a.opts.ProductKey,
},
{
URL: "tenant",
ValueString: tenantId,
},
},
},
},
},
}

auditMessage, err := json.Marshal(auditObject)
if err != nil {
logger.Errorf("%s: could not marshal the audit object: %v", ErrPersitAuditEvent.Error(), err)
return
}
err = a.send(string(auditMessage))
if err != nil {
logger.Errorf("%s: could not send the audit message to the url '%s': %v", ErrPersitAuditEvent.Error(), a.opts.Url, err)
return
}
}

func (c *AuditClient) send(msg string) error {
signedDate := time.Now().UTC().Format(time.RFC3339)
signature := c.apiSignature.GetSignature(signedDate)
resp, err := c.client.R().
SetHeader("Content-Type", "application/json").
SetHeader("api-version", "2").
SetHeader("HSDP-API-Signature", signature).
SetHeader("SignedDate", signedDate).
SetBody(msg).
Post(c.opts.Url)
if err != nil {
return err
}
if resp.StatusCode() != 201 {
log.Println("Not able to send the audit message ", resp)
return fmt.Errorf("not able to persist audit, audit server returned %v", resp.StatusCode())
}
return nil
}

func (a *AuditClientOpts) Validate() error {
if strings.TrimSpace(a.ProductName) == "" || strings.TrimSpace(a.ProductKey) == "" || strings.TrimSpace(a.SecretKey) == "" || strings.TrimSpace(a.SharedKey) == "" {
return errors.New("the audit is enabled and therefore the audit product name, audit key, audit secret key or audit shared key are required (however found empty)")
}
return nil
}
7 changes: 7 additions & 0 deletions pkg/audit/audit_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package audit

import "errors"

var (
ErrPersitAuditEvent = errors.New("could not persist the audit event")
)
116 changes: 116 additions & 0 deletions pkg/audit/audit_event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package audit

type Coding struct {
System string `json:"system,omitempty"`
Version string `json:"version,omitempty"`
Code string `json:"code,omitempty"`
Display string `json:"display,omitempty"`
UserSelected string `json:"userSelected,omitempty"`
}

type Identifier struct {
Use string `json:"use,omitempty"`
Type *Coding `json:"type,omitempty"`
Value string `json:"value,omitempty"`
System string `json:"system,omitempty"`
}

type Reference struct {
Reference string `json:"reference,omitempty"`
Display string `json:"display,omitempty"`
}

type Detail struct {
Type string `json:"type,omitempty"`
Value string `json:"value,omitempty"`
}

type Object struct {
Identifier *Identifier `json:"identifier,omitempty"`
Reference *Reference `json:"reference,omitempty"`
Type *Coding `json:"type,omitempty"`
Role *Coding `json:"role,omitempty"`
Lifecycle *Coding `json:"lifecycle,omitempty"`
SecurityLabel []Coding `json:"securityLabel,omitempty"`
Description string `json:"description,omitempty"`
Query interface{} `json:"query,omitempty"`
Name string `json:"name,omitempty"`
Detail []Detail `json:"detail,omitempty"`
}

type Type struct {
Coding `json:"coding,omitempty"`
Text string `json:"text,omitempty"`
}

type UserID struct {
Use string `json:"use,omitempty"`
Type *Type `json:"type,omitempty"`
System string `json:"system,omitempty"`
Value string `json:"value,omitempty"`
}

type Network struct {
Address string `json:"address,omitempty"`
Type string `json:"type,omitempty"`
}

type Media struct {
*Coding `json:"coding,omitempty"`
}

type PurposeOfUse struct {
*Coding `json:"coding,omitempty"`
}

type Participant struct {
Role []Type `json:"role,omitempty"`
Reference *Reference `json:"reference,omitempty"`
UserID UserID `json:"userId,omitempty"`
AltID string `json:"altId,omitempty"`
Name string `json:"name,omitempty"`
Requestor bool `json:"requestor,omitempty"`
Location *Reference `json:"location,omitempty"`
Policy []string `json:"policy,omitempty"`
Media *Media `json:"media,omitempty"`
Network *Network `json:"network,omitempty"`
PurposeOfUse []PurposeOfUse `json:"purposeOfUse,omitempty"`
}

type ExtensionContent struct {
URL string `json:"url,omitempty"`
ValueString string `json:"valueString,omitempty"`
}
type Extension struct {
URL string `json:"url,omitempty"`
Extension []*ExtensionContent `json:"extension,omitempty"`
}

type Source struct {
Site string `json:"site,omitempty"`
Identifier Identifier `json:"identifier,omitempty"`
Type []*Coding `json:"type,omitempty"`
Extension []*Extension `json:"extension,omitempty"`
}

type PurposeOfEvent struct {
Coding `json:"coding,omitempty"`
}

type Event struct {
Type *Coding `json:"type,omitempty"`
Subtype []*Coding `json:"subtype,omitempty"`
Action string `json:"action,omitempty"`
DateTime string `json:"dateTime,omitempty"`
Outcome string `json:"outcome,omitempty"`
OutcomeDesc string `json:"outcomeDesc,omitempty"`
PurposeOfEvent []*PurposeOfEvent `json:"purposeOfEvent,omitempty"`
}

type AuditEvent struct {
ResourceType string `json:"resourceType,omitempty"`
Event *Event `json:"event,omitempty"`
Participant []*Participant `json:"participant,omitempty"`
Source Source `json:"source,omitempty"`
Object []*Object `json:"object,omitempty"`
}
Loading

0 comments on commit 85b4dca

Please sign in to comment.