Skip to content

Commit

Permalink
feat: add location based on ip to the audit log
Browse files Browse the repository at this point in the history
  • Loading branch information
stonith404 committed Oct 4, 2024
1 parent e033ba6 commit 025378d
Show file tree
Hide file tree
Showing 18 changed files with 110 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/build-and-push-docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}

- name: Download GeoLite2 City database
run: MAXMIND_LICENSE_KEY=${{ secrets.MAXMIND_LICENSE_KEY }} sh scripts/download-ip-database.sh

- name: Build and push
uses: docker/build-push-action@v4
with:
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ vite.config.ts.timestamp-*
# Application specific
data
/frontend/tests/.auth
pocket-id-backend
pocket-id-backend
/backend/GeoLite2-City.mmdb
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ COPY --from=frontend-builder /app/frontend/package.json ./frontend/package.json

COPY --from=backend-builder /app/backend/pocket-id-backend ./backend/pocket-id-backend
COPY --from=backend-builder /app/backend/migrations ./backend/migrations
COPY --from=backend-builder /app/backend/GeoLite2-City.mmdb ./backend/GeoLite2-City.mmdb
COPY --from=backend-builder /app/backend/email-templates ./backend/email-templates
COPY --from=backend-builder /app/backend/images ./backend/images

Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ Required tools:
cd ..
pm2 start pocket-id-backend --name pocket-id-backend

# Optional: Download the GeoLite2 city database.
# If not downloaded the ip location in the audit log will be empty.
MAXMIND_LICENSE_KEY=<your-key> sh scripts/download-ip-database.sh

# Start the frontend
cd ../frontend
npm install
Expand All @@ -94,7 +98,6 @@ You may need the following information:
- **Userinfo URL**: `https://<your-domain>/api/oidc/userinfo`
- **Certificate URL**: `https://<your-domain>/.well-known/jwks.json`
- **OIDC Discovery URL**: `https://<your-domain>/.well-known/openid-configuration`
- **PKCE**: `false` as this is not supported yet.
- **Scopes**: At least `openid email`. Optionally you can add `profile` and `groups`.

### Proxy Services with Pocket ID
Expand Down Expand Up @@ -132,6 +135,9 @@ docker compose up -d
cd ..
pm2 start pocket-id-backend --name pocket-id-backend

# Optional: Update the GeoLite2 city database
MAXMIND_LICENSE_KEY=<your-key> sh scripts/download-ip-database.sh

# Start the frontend
cd ../frontend
npm install
Expand Down
12 changes: 9 additions & 3 deletions backend/email-templates/login-with-new-device_html.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,28 @@
<div class="content">
<h2>New Sign-In Detected</h2>
<div class="grid">
{{ if and .Data.City .Data.Country }}
<div>
<p class="label">Approximate Location</p>
<p>{{ .Data.City }}, {{ .Data.Country }}</p>
</div>
{{ end }}
<div>
<p class="label">IP Address</p>
<p>{{ .Data.IPAddress}}</p>
<p>{{ .Data.IPAddress }}</p>
</div>
<div>
<p class="label">Device</p>
<p>{{ .Data.Device }}</p>
</div>
<div>
<p class="label">Sign-In Time</p>
<p>{{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC"}}</p>
<p>{{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC" }}</p>
</div>
</div>
<p class="message">
This sign-in was detected from a new device or location. If you recognize this activity, you can
safely ignore this message. If not, please review your account and security settings.
</p>
</div>
{{ end -}}
{{ end -}}
3 changes: 3 additions & 0 deletions backend/email-templates/login-with-new-device_text.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
New Sign-In Detected
====================

{{ if and .Data.City .Data.Country }}
Approximate Location: {{ .Data.City }}, {{ .Data.Country }}
{{ end }}
IP Address: {{ .Data.IPAddress }}
Device: {{ .Data.Device }}
Time: {{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC"}}
Expand Down
1 change: 1 addition & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/mileusna/useragent v1.3.4
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1
golang.org/x/crypto v0.26.0
golang.org/x/time v0.6.0
gorm.io/driver/sqlite v1.5.6
Expand Down
2 changes: 2 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1 h1:UihPOz+oIJ5X0JsO7wEkL50fheCODsoZ9r86mJWfNMc=
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1/go.mod h1:vPpFrres6g9B5+meBwAd9xnp335KFcLEFW7EqJxBHy0=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
Expand Down
2 changes: 2 additions & 0 deletions backend/internal/dto/audit_log_dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type AuditLogDto struct {

Event model.AuditLogEvent `json:"event"`
IpAddress string `json:"ipAddress"`
Country string `json:"country"`
City string `json:"city"`
Device string `json:"device"`
UserID string `json:"userID"`
Data model.AuditLogData `json:"data"`
Expand Down
2 changes: 2 additions & 0 deletions backend/internal/model/audit_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type AuditLog struct {

Event AuditLogEvent
IpAddress string
Country string
City string
UserAgent string
UserID string
Data AuditLogData
Expand Down
37 changes: 37 additions & 0 deletions backend/internal/service/audit_log_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package service

import (
userAgentParser "github.com/mileusna/useragent"
"github.com/oschwald/maxminddb-golang/v2"
"github.com/stonith404/pocket-id/backend/internal/model"
"github.com/stonith404/pocket-id/backend/internal/utils"
"github.com/stonith404/pocket-id/backend/internal/utils/email"
"gorm.io/gorm"
"log"
"net/netip"
)

type AuditLogService struct {
Expand All @@ -21,9 +23,16 @@ func NewAuditLogService(db *gorm.DB, appConfigService *AppConfigService, emailSe

// Create creates a new audit log entry in the database
func (s *AuditLogService) Create(event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData) model.AuditLog {
country, city, err := s.GetIpLocation(ipAddress)
if err != nil {
log.Printf("Failed to get IP location: %v\n", err)
}

auditLog := model.AuditLog{
Event: event,
IpAddress: ipAddress,
Country: country,
City: city,
UserAgent: userAgent,
UserID: userID,
Data: data,
Expand Down Expand Up @@ -61,6 +70,8 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ipAddress, userAgent, userID
Email: user.Email,
}, NewLoginTemplate, &NewLoginTemplateData{
IPAddress: ipAddress,
Country: createdAuditLog.Country,
City: createdAuditLog.City,
Device: s.DeviceStringFromUserAgent(userAgent),
DateTime: createdAuditLog.CreatedAt.UTC(),
})
Expand All @@ -86,3 +97,29 @@ func (s *AuditLogService) DeviceStringFromUserAgent(userAgent string) string {
ua := userAgentParser.Parse(userAgent)
return ua.Name + " on " + ua.OS + " " + ua.OSVersion
}

func (s *AuditLogService) GetIpLocation(ipAddress string) (country, city string, err error) {
db, err := maxminddb.Open("GeoLite2-City.mmdb")
if err != nil {
return "", "", err
}
defer db.Close()

addr := netip.MustParseAddr(ipAddress)

var record struct {
City struct {
Names map[string]string `maxminddb:"names"`
} `maxminddb:"city"`
Country struct {
Names map[string]string `maxminddb:"names"`
} `maxminddb:"country"`
}

err = db.Lookup(addr).Decode(&record)
if err != nil {
return "", "", err
}

return record.Country.Names["en"], record.City.Names["en"], nil
}
2 changes: 2 additions & 0 deletions backend/internal/service/email_service_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ var NewLoginTemplate = email.Template[NewLoginTemplateData]{

type NewLoginTemplateData struct {
IPAddress string
Country string
City string
Device string
DateTime time.Time
}
Expand Down
2 changes: 2 additions & 0 deletions backend/migrations/20241004092030_audit_log_location.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE audit_logs DROP COLUMN country;
ALTER TABLE audit_logs DROP COLUMN city;
2 changes: 2 additions & 0 deletions backend/migrations/20241004092030_audit_log_location.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE audit_logs ADD COLUMN country TEXT;
ALTER TABLE audit_logs ADD COLUMN city TEXT;
2 changes: 2 additions & 0 deletions frontend/src/lib/types/audit-log.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export type AuditLog = {
id: string;
event: string;
ipAddress: string;
country?: string;
city?: string;
device: string;
createdAt: string;
data: any;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
'Token URL': `https://${$page.url.hostname}/api/oidc/token`,
'Userinfo URL': `https://${$page.url.hostname}/api/oidc/userinfo`,
'Certificate URL': `https://${$page.url.hostname}/.well-known/jwks.json`,
PKCE: 'Disabled'
};
async function updateClient(updatedClient: OidcClientCreateWithLogo) {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/routes/settings/audit-log/audit-log-list.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<Table.Row>
<Table.Head>Time</Table.Head>
<Table.Head>Event</Table.Head>
<Table.Head>Approximate Location</Table.Head>
<Table.Head>IP Address</Table.Head>
<Table.Head>Device</Table.Head>
<Table.Head>Client</Table.Head>
Expand All @@ -47,6 +48,7 @@
<Table.Cell>
<Badge variant="outline">{toFriendlyEventString(auditLog.event)}</Badge>
</Table.Cell>
<Table.Cell>{auditLog.city && auditLog.country ? `${auditLog.city}, ${auditLog.country}` : 'Unknown'}</Table.Cell>
<Table.Cell>{auditLog.ipAddress}</Table.Cell>
<Table.Cell>{auditLog.device}</Table.Cell>
<Table.Cell>{auditLog.data.clientName}</Table.Cell>
Expand Down
31 changes: 31 additions & 0 deletions scripts/download-ip-database.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash

# Check if the license key environment variable is set
if [ -z "$MAXMIND_LICENSE_KEY" ]; then
echo "Error: MAXMIND_LICENSE_KEY environment variable is not set."
echo "Please set it using 'export MAXMIND_LICENSE_KEY=your_license_key' and try again."
exit 1
fi
echo $MAXMIND_LICENSE_KEY
# GeoLite2 City Database URL
URL="https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz"

# Download directory
DOWNLOAD_DIR="./geolite2_db"
TARGET_PATH=./backend/GeoLite2-City.mmdb
mkdir -p $DOWNLOAD_DIR

# Download the database
echo "Downloading GeoLite2 City database..."
curl -L -o "$DOWNLOAD_DIR/GeoLite2-City.tar.gz" "$URL"

# Extract the downloaded file
echo "Extracting GeoLite2 City database..."
tar -xzf "$DOWNLOAD_DIR/GeoLite2-City.tar.gz" -C $DOWNLOAD_DIR --strip-components=1

mv "$DOWNLOAD_DIR/GeoLite2-City.mmdb" $TARGET_PATH

# Clean up
rm -rf "$DOWNLOAD_DIR"

echo "GeoLite2 City database downloaded and extracted to $TARGET_PATH"

0 comments on commit 025378d

Please sign in to comment.