From 025378d14edd2d72da76e90799a0ccdd42cf672c Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Fri, 4 Oct 2024 12:11:10 +0200 Subject: [PATCH] feat: add location based on ip to the audit log --- .../workflows/build-and-push-docker-image.yml | 3 ++ .gitignore | 3 +- Dockerfile | 1 + README.md | 8 +++- .../login-with-new-device_html.tmpl | 12 ++++-- .../login-with-new-device_text.tmpl | 3 ++ backend/go.mod | 1 + backend/go.sum | 2 + backend/internal/dto/audit_log_dto.go | 2 + backend/internal/model/audit_log.go | 2 + backend/internal/service/audit_log_service.go | 37 +++++++++++++++++++ .../service/email_service_templates.go | 2 + ...20241004092030_audit_log_location.down.sql | 2 + .../20241004092030_audit_log_location.up.sql | 2 + frontend/src/lib/types/audit-log.type.ts | 2 + .../admin/oidc-clients/[id]/+page.svelte | 1 - .../settings/audit-log/audit-log-list.svelte | 2 + scripts/download-ip-database.sh | 31 ++++++++++++++++ 18 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 backend/migrations/20241004092030_audit_log_location.down.sql create mode 100644 backend/migrations/20241004092030_audit_log_location.up.sql create mode 100644 scripts/download-ip-database.sh diff --git a/.github/workflows/build-and-push-docker-image.yml b/.github/workflows/build-and-push-docker-image.yml index 0649d74..faff502 100644 --- a/.github/workflows/build-and-push-docker-image.yml +++ b/.github/workflows/build-and-push-docker-image.yml @@ -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: diff --git a/.gitignore b/.gitignore index fe485c8..db1bed5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ vite.config.ts.timestamp-* # Application specific data /frontend/tests/.auth -pocket-id-backend \ No newline at end of file +pocket-id-backend +/backend/GeoLite2-City.mmdb \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e2a8da6..6d7087e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index 5e7e49c..c2dd463 100644 --- a/README.md +++ b/README.md @@ -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= sh scripts/download-ip-database.sh + # Start the frontend cd ../frontend npm install @@ -94,7 +98,6 @@ You may need the following information: - **Userinfo URL**: `https:///api/oidc/userinfo` - **Certificate URL**: `https:///.well-known/jwks.json` - **OIDC Discovery URL**: `https:///.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 @@ -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= sh scripts/download-ip-database.sh + # Start the frontend cd ../frontend npm install diff --git a/backend/email-templates/login-with-new-device_html.tmpl b/backend/email-templates/login-with-new-device_html.tmpl index 4a6cde4..c911e83 100644 --- a/backend/email-templates/login-with-new-device_html.tmpl +++ b/backend/email-templates/login-with-new-device_html.tmpl @@ -9,9 +9,15 @@

New Sign-In Detected

+ {{ if and .Data.City .Data.Country }} +
+

Approximate Location

+

{{ .Data.City }}, {{ .Data.Country }}

+
+ {{ end }}

IP Address

-

{{ .Data.IPAddress}}

+

{{ .Data.IPAddress }}

Device

@@ -19,7 +25,7 @@

Sign-In Time

-

{{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC"}}

+

{{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC" }}

@@ -27,4 +33,4 @@ safely ignore this message. If not, please review your account and security settings.

-{{ end -}} +{{ end -}} \ No newline at end of file diff --git a/backend/email-templates/login-with-new-device_text.tmpl b/backend/email-templates/login-with-new-device_text.tmpl index a89e6a0..ca7f1b0 100644 --- a/backend/email-templates/login-with-new-device_text.tmpl +++ b/backend/email-templates/login-with-new-device_text.tmpl @@ -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"}} diff --git a/backend/go.mod b/backend/go.mod index eb04753..4fce2b5 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -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 diff --git a/backend/go.sum b/backend/go.sum index e728d1c..68c26b7 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -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= diff --git a/backend/internal/dto/audit_log_dto.go b/backend/internal/dto/audit_log_dto.go index 65d6c55..1416af8 100644 --- a/backend/internal/dto/audit_log_dto.go +++ b/backend/internal/dto/audit_log_dto.go @@ -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"` diff --git a/backend/internal/model/audit_log.go b/backend/internal/model/audit_log.go index cf392e1..736b006 100644 --- a/backend/internal/model/audit_log.go +++ b/backend/internal/model/audit_log.go @@ -11,6 +11,8 @@ type AuditLog struct { Event AuditLogEvent IpAddress string + Country string + City string UserAgent string UserID string Data AuditLogData diff --git a/backend/internal/service/audit_log_service.go b/backend/internal/service/audit_log_service.go index 0565b0c..f01193b 100644 --- a/backend/internal/service/audit_log_service.go +++ b/backend/internal/service/audit_log_service.go @@ -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 { @@ -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, @@ -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(), }) @@ -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 +} diff --git a/backend/internal/service/email_service_templates.go b/backend/internal/service/email_service_templates.go index 3f8ca2b..77c3393 100644 --- a/backend/internal/service/email_service_templates.go +++ b/backend/internal/service/email_service_templates.go @@ -29,6 +29,8 @@ var NewLoginTemplate = email.Template[NewLoginTemplateData]{ type NewLoginTemplateData struct { IPAddress string + Country string + City string Device string DateTime time.Time } diff --git a/backend/migrations/20241004092030_audit_log_location.down.sql b/backend/migrations/20241004092030_audit_log_location.down.sql new file mode 100644 index 0000000..ed8a44a --- /dev/null +++ b/backend/migrations/20241004092030_audit_log_location.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE audit_logs DROP COLUMN country; +ALTER TABLE audit_logs DROP COLUMN city; \ No newline at end of file diff --git a/backend/migrations/20241004092030_audit_log_location.up.sql b/backend/migrations/20241004092030_audit_log_location.up.sql new file mode 100644 index 0000000..5e22bce --- /dev/null +++ b/backend/migrations/20241004092030_audit_log_location.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE audit_logs ADD COLUMN country TEXT; +ALTER TABLE audit_logs ADD COLUMN city TEXT; \ No newline at end of file diff --git a/frontend/src/lib/types/audit-log.type.ts b/frontend/src/lib/types/audit-log.type.ts index 583843e..e9d024e 100644 --- a/frontend/src/lib/types/audit-log.type.ts +++ b/frontend/src/lib/types/audit-log.type.ts @@ -2,6 +2,8 @@ export type AuditLog = { id: string; event: string; ipAddress: string; + country?: string; + city?: string; device: string; createdAt: string; data: any; diff --git a/frontend/src/routes/settings/admin/oidc-clients/[id]/+page.svelte b/frontend/src/routes/settings/admin/oidc-clients/[id]/+page.svelte index 87b163f..040868f 100644 --- a/frontend/src/routes/settings/admin/oidc-clients/[id]/+page.svelte +++ b/frontend/src/routes/settings/admin/oidc-clients/[id]/+page.svelte @@ -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) { diff --git a/frontend/src/routes/settings/audit-log/audit-log-list.svelte b/frontend/src/routes/settings/audit-log/audit-log-list.svelte index 5da2bb4..f12bd8c 100644 --- a/frontend/src/routes/settings/audit-log/audit-log-list.svelte +++ b/frontend/src/routes/settings/audit-log/audit-log-list.svelte @@ -30,6 +30,7 @@ Time Event + Approximate Location IP Address Device Client @@ -47,6 +48,7 @@ {toFriendlyEventString(auditLog.event)} + {auditLog.city && auditLog.country ? `${auditLog.city}, ${auditLog.country}` : 'Unknown'} {auditLog.ipAddress} {auditLog.device} {auditLog.data.clientName} diff --git a/scripts/download-ip-database.sh b/scripts/download-ip-database.sh new file mode 100644 index 0000000..1971849 --- /dev/null +++ b/scripts/download-ip-database.sh @@ -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" \ No newline at end of file