server is launchable and passkey support works
This commit is contained in:
Melody Becker 2024-09-27 16:53:22 +02:00
parent ee172d84a8
commit c572066571
21 changed files with 857 additions and 40 deletions

26
authentication-flow.md Normal file
View file

@ -0,0 +1,26 @@
# Plan for how authentication will work
## Frontend auth
### Registration
1. Send username to registration endpoint
2. Get webauthn options
3. Perform webauthn check (selecting and confirming passkey)
4. Server verifies response
5. Minimal account ready for login
### Login
1. Send username to login endpoint
2. Error out if user doesn't exist
3. Get webauthn options from response
4. Get passkey response
5. Send response to Server
6. Server checks and replies with session token
7. Frontend uses session token for authorisation of all requests afterwards
## api
1. Generate API token via frontend
2. Use api token for authorisation

View file

13
cmd/test/main.go Normal file
View file

@ -0,0 +1,13 @@
package main
import (
"embed"
"fmt"
)
//go:embed example
var fs embed.FS
func main() {
fmt.Println(fs.ReadDir("example"))
}

BIN
cmd/test/test Executable file

Binary file not shown.

View file

@ -2,7 +2,6 @@
services:
postgres:
image: docker.io/postgres:16.4
restart: unless-stopped
environment:
POSTGRES_PASSWORD: linstrom
POSTGRES_USER: linstrom

View file

@ -3,5 +3,6 @@
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
*/
"isTypeScriptProject": true
"isTypeScriptProject": true,
"pnpm": true
}

View file

@ -73,6 +73,7 @@
"ember-page-title": "^8.2.3",
"ember-qunit": "^8.1.0",
"ember-resolver": "^11.0.1",
"ember-simple-auth": "^6.1.0",
"ember-source": "~5.11.0",
"ember-template-lint": "^5.13.0",
"ember-tooltips": "^3.6.0",
@ -163,6 +164,35 @@
"url": "https://opencollective.com/babel"
}
},
"node_modules/@babel/eslint-parser": {
"version": "7.25.1",
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.1.tgz",
"integrity": "sha512-Y956ghgTT4j7rKesabkh5WeqgSFZVFwaPR0IWFm7KFHFmmJ4afbG49SmfW4S+GyRPx0Dy5jxEWA5t0rpxfElWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
"eslint-visitor-keys": "^2.1.0",
"semver": "^6.3.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || >=14.0.0"
},
"peerDependencies": {
"@babel/core": "^7.11.0",
"eslint": "^7.5.0 || ^8.0.0 || ^9.0.0"
}
},
"node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=10"
}
},
"node_modules/@babel/generator": {
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz",
@ -5844,6 +5874,40 @@
"node": "12.* || >= 14"
}
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
"integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-scope": "5.1.1"
}
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -15512,6 +15576,19 @@
"semver": "bin/semver"
}
},
"node_modules/ember-cookies": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/ember-cookies/-/ember-cookies-1.1.2.tgz",
"integrity": "sha512-6GaN0eEDZT9SEUSZBxWzZMlvxjcGKXFTJNjv30LVXTTOxozE5IBmIxiDAEq0udi0UpWUGHLYQBgnANn4jdll7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@embroider/addon-shim": "^1.7.1"
},
"engines": {
"node": ">= 16.*"
}
},
"node_modules/ember-data": {
"version": "5.3.8",
"resolved": "https://registry.npmjs.org/ember-data/-/ember-data-5.3.8.tgz",
@ -20172,6 +20249,30 @@
"node": "8.* || 10.* || >= 12"
}
},
"node_modules/ember-simple-auth": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ember-simple-auth/-/ember-simple-auth-6.1.0.tgz",
"integrity": "sha512-LhOl7TrOKlqb+0a/5STOoTSncDNuPELuFZ9+1SLduVX7DtdQr8VOEAmB8UaOnG0clJ9Bj6E3SczhXGjqd718Lw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/eslint-parser": "^7.24.7",
"@ember/test-waiters": "^3",
"@embroider/addon-shim": "^1.0.0",
"@embroider/macros": "^1.0.0",
"ember-cli-is-package-missing": "^1.0.0",
"ember-cookies": "^1.0.0",
"silent-error": "^1.0.0"
},
"peerDependencies": {
"@ember/test-helpers": ">= 3 || > 2.7"
},
"peerDependenciesMeta": {
"@ember/test-helpers": {
"optional": true
}
}
},
"node_modules/ember-source": {
"version": "5.11.0",
"resolved": "https://registry.npmjs.org/ember-source/-/ember-source-5.11.0.tgz",

View file

@ -87,6 +87,7 @@
"ember-page-title": "^8.2.3",
"ember-qunit": "^8.1.0",
"ember-resolver": "^11.0.1",
"ember-simple-auth": "^6.1.0",
"ember-source": "~5.11.0",
"ember-template-lint": "^5.13.0",
"ember-tooltips": "^3.6.0",

16
go.mod
View file

@ -6,11 +6,13 @@ toolchain go1.23.0
require (
github.com/BurntSushi/toml v1.4.0
github.com/aws/aws-sdk-go v1.55.5
github.com/dgraph-io/ristretto v0.1.1
github.com/eko/gocache/lib/v4 v4.1.6
github.com/eko/gocache/store/redis/v4 v4.2.2
github.com/eko/gocache/store/ristretto/v4 v4.2.2
github.com/glebarez/sqlite v1.11.0
github.com/gabriel-vasile/mimetype v1.4.5
github.com/gen2brain/avif v0.3.2
github.com/go-webauthn/webauthn v0.11.2
github.com/google/uuid v1.6.0
github.com/mstarongithub/passkey v0.0.0-20240817142622-de6912c8303e
@ -19,6 +21,7 @@ require (
github.com/xhit/go-simple-mail/v2 v2.16.0
gitlab.com/mstarongitlab/goap v1.1.0
gitlab.com/mstarongitlab/goutils v1.3.0
golang.org/x/image v0.20.0
gorm.io/driver/postgres v1.5.7
gorm.io/gorm v1.25.10
)
@ -43,15 +46,11 @@ require (
)
require (
github.com/aws/aws-sdk-go v1.55.5 // indirect
github.com/datainq/xml-date-time v0.0.0-20170820214645-2292f08baa38 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.7.1 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/gen2brain/avif v0.3.2 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-test/deep v1.1.1 // indirect
github.com/go-webauthn/x v0.1.14 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
@ -67,21 +66,16 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/piprate/json-gold v0.5.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tetratelabs/wazero v1.7.3 // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/image v0.20.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/text v0.18.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
)

22
go.sum
View file

@ -93,10 +93,6 @@ github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/gen2brain/avif v0.3.2 h1:XUR0CBl5n4ISFJE8/pc1RMEKt5KUVoW8InctN+M7+DQ=
github.com/gen2brain/avif v0.3.2/go.mod h1:tdL2sV6oOJXBZZvT5iP55VEM1X2c3/yJmYKMJTl8fXg=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -175,8 +171,6 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -197,6 +191,7 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@ -273,12 +268,10 @@ github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSxfE=
github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@ -467,8 +460,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -609,6 +600,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
@ -624,14 +616,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

51
main.go
View file

@ -2,18 +2,32 @@
package main
import (
"embed"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/mstarongithub/passkey"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gitlab.com/mstarongitlab/linstrom/ap"
// "gitlab.com/mstarongitlab/linstrom/ap"
"gitlab.com/mstarongitlab/linstrom/config"
"gitlab.com/mstarongitlab/linstrom/server"
"gitlab.com/mstarongitlab/linstrom/storage"
"gitlab.com/mstarongitlab/linstrom/storage/cache"
"gitlab.com/mstarongitlab/linstrom/util"
)
//go:embed frontend-reactive/dist/* frontend-reactive/dist/assets
var reactiveFS embed.FS
//go:embed frontend-noscript
var nojsFS embed.FS
func main() {
setLogger()
setLogLevel()
@ -23,11 +37,11 @@ func main() {
Str("config-file", *flagConfigFile).
Msg("Failed to read config and couldn't write default")
}
res, err := ap.GetAccountWebfinger("@aufricus_athudath@activitypub.academy")
log.Info().
Err(err).
Any("webfinger", res).
Msg("Webfinger request result for @aufricus_athudath@activitypub.academy")
// res, err := ap.GetAccountWebfinger("@aufricus_athudath@activitypub.academy")
// log.Info().
// Err(err).
// Any("webfinger", res).
// Msg("Webfinger request result for @aufricus_athudath@activitypub.academy")
storageCache, err := cache.NewCache(
config.GlobalConfig.Storage.MaxInMemoryCacheSize,
config.GlobalConfig.Storage.RedisUrl,
@ -48,7 +62,30 @@ func main() {
if err != nil {
log.Fatal().Err(err).Msg("Failed to setup storage")
}
_ = store
pkey, err := passkey.New(passkey.Config{
WebauthnConfig: &webauthn.Config{
RPDisplayName: "Linstrom",
RPID: "localhost",
RPOrigins: []string{"http://localhost:8000"},
},
UserStore: store,
SessionStore: store,
SessionMaxAge: time.Hour * 24,
}, passkey.WithLogger(&util.ZerologWrapper{}))
if err != nil {
log.Fatal().Err(err).Msg("Failed to setup passkey support")
}
fmt.Println(nojsFS.ReadDir("frontend-noscript"))
server := server.NewServer(
store,
pkey,
util.NewFSWrapper(reactiveFS, "frontend-reactive/dist/", true),
util.NewFSWrapper(nojsFS, "frontend-noscript/", true),
)
server.Start(":8000")
// TODO: Set up media server
// TODO: Set up queues
// TODO: Set up http server

330
pk-auth/index.es5.umd.min.js vendored Normal file
View file

@ -0,0 +1,330 @@
/* [@simplewebauthn/browser@10.0.0] */
!(function(e, t) {
"object" == typeof exports && "undefined" != typeof module
? t(exports)
: "function" == typeof define && define.amd
? define(["exports"], t)
: t(
((e =
"undefined" != typeof globalThis
? globalThis
: e || self).SimpleWebAuthnBrowser = {}),
);
})(this, function(e) {
"use strict";
function t(e) {
const t = new Uint8Array(e);
let r = "";
for (const e of t) r += String.fromCharCode(e);
return btoa(r).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
function r(e) {
console.trace(e);
const t = e.replace(/-/g, "+").replace(/_/g, "/"),
r = (4 - (t.length % 4)) % 4,
n = t.padEnd(t.length + r, "="),
o = atob(n),
i = new ArrayBuffer(o.length),
a = new Uint8Array(i);
for (let e = 0; e < o.length; e++) a[e] = o.charCodeAt(e);
return i;
}
function n() {
return (
void 0 !== window?.PublicKeyCredential &&
"function" == typeof window.PublicKeyCredential
);
}
function o(e) {
const { id: t } = e;
return { ...e, id: r(t), transports: e.transports };
}
function i(e) {
return (
"localhost" === e || /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(e)
);
}
class a extends Error {
constructor({ message: e, code: t, cause: r, name: n }) {
super(e, { cause: r }), (this.name = n ?? r.name), (this.code = t);
}
}
const s = new (class {
createNewAbortSignal() {
if (this.controller) {
const e = new Error(
"Cancelling existing WebAuthn API call for new one",
);
(e.name = "AbortError"), this.controller.abort(e);
}
const e = new AbortController();
return (this.controller = e), e.signal;
}
cancelCeremony() {
if (this.controller) {
const e = new Error("Manually cancelling existing WebAuthn API call");
(e.name = "AbortError"),
this.controller.abort(e),
(this.controller = void 0);
}
}
})(),
c = ["cross-platform", "platform"];
function l(e) {
if (e && !(c.indexOf(e) < 0)) return e;
}
function u(e, t) {
console.warn(
`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${e}. You should report this error to them.\n`,
t,
);
}
function d() {
if (!n()) return new Promise((e) => e(!1));
const e = window.PublicKeyCredential;
return void 0 === e.isConditionalMediationAvailable
? new Promise((e) => e(!1))
: e.isConditionalMediationAvailable();
}
(e.WebAuthnAbortService = s),
(e.WebAuthnError = a),
(e.base64URLStringToBuffer = r),
(e.browserSupportsWebAuthn = n),
(e.browserSupportsWebAuthnAutofill = d),
(e.bufferToBase64URLString = t),
(e.platformAuthenticatorIsAvailable = function() {
return n()
? PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
: new Promise((e) => e(!1));
}),
(e.startAuthentication = async function(e, c = !1) {
if (!n()) throw new Error("WebAuthn is not supported in this browser");
let u;
0 !== e.allowCredentials?.length && (u = e.allowCredentials?.map(o));
const h = { ...e, challenge: r(e.challenge), allowCredentials: u },
f = {};
if (c) {
if (!(await d()))
throw Error("Browser does not support WebAuthn autofill");
if (
document.querySelectorAll("input[autocomplete$='webauthn']").length <
1
)
throw Error(
'No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected',
);
(f.mediation = "conditional"), (h.allowCredentials = []);
}
let p;
(f.publicKey = h), (f.signal = s.createNewAbortSignal());
try {
p = await navigator.credentials.get(f);
} catch (e) {
throw (function({ error: e, options: t }) {
const { publicKey: r } = t;
if (!r)
throw Error("options was missing required publicKey property");
if ("AbortError" === e.name) {
if (t.signal instanceof AbortSignal)
return new a({
message: "Authentication ceremony was sent an abort signal",
code: "ERROR_CEREMONY_ABORTED",
cause: e,
});
} else {
if ("NotAllowedError" === e.name)
return new a({
message: e.message,
code: "ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",
cause: e,
});
if ("SecurityError" === e.name) {
const t = window.location.hostname;
if (!i(t))
return new a({
message: `${window.location.hostname} is an invalid domain`,
code: "ERROR_INVALID_DOMAIN",
cause: e,
});
if (r.rpId !== t)
return new a({
message: `The RP ID "${r.rpId}" is invalid for this domain`,
code: "ERROR_INVALID_RP_ID",
cause: e,
});
} else if ("UnknownError" === e.name)
return new a({
message:
"The authenticator was unable to process the specified options, or could not create a new assertion signature",
code: "ERROR_AUTHENTICATOR_GENERAL_ERROR",
cause: e,
});
}
return e;
})({ error: e, options: f });
}
if (!p) throw new Error("Authentication was not completed");
const { id: R, rawId: w, response: E, type: g } = p;
let A;
return (
E.userHandle && (A = t(E.userHandle)),
{
id: R,
rawId: t(w),
response: {
authenticatorData: t(E.authenticatorData),
clientDataJSON: t(E.clientDataJSON),
signature: t(E.signature),
userHandle: A,
},
type: g,
clientExtensionResults: p.getClientExtensionResults(),
authenticatorAttachment: l(p.authenticatorAttachment),
}
);
}),
(e.startRegistration = async function(e) {
if (!n()) throw new Error("WebAuthn is not supported in this browser");
console.log("StartRegVal", e);
const c = {
publicKey: {
...e,
challenge: r(e.challenge),
user: { ...e.user, id: r(e.user.id) },
excludeCredentials: e.excludeCredentials?.map(o),
},
};
let d;
c.signal = s.createNewAbortSignal();
try {
d = await navigator.credentials.create(c);
} catch (e) {
throw (function({ error: e, options: t }) {
const { publicKey: r } = t;
if (!r)
throw Error("options was missing required publicKey property");
if ("AbortError" === e.name) {
if (t.signal instanceof AbortSignal)
return new a({
message: "Registration ceremony was sent an abort signal",
code: "ERROR_CEREMONY_ABORTED",
cause: e,
});
} else if ("ConstraintError" === e.name) {
if (!0 === r.authenticatorSelection?.requireResidentKey)
return new a({
message:
"Discoverable credentials were required but no available authenticator supported it",
code: "ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT",
cause: e,
});
if ("required" === r.authenticatorSelection?.userVerification)
return new a({
message:
"User verification was required but no available authenticator supported it",
code: "ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT",
cause: e,
});
} else {
if ("InvalidStateError" === e.name)
return new a({
message: "The authenticator was previously registered",
code: "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED",
cause: e,
});
if ("NotAllowedError" === e.name)
return new a({
message: e.message,
code: "ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",
cause: e,
});
if ("NotSupportedError" === e.name)
return 0 ===
r.pubKeyCredParams.filter((e) => "public-key" === e.type).length
? new a({
message:
'No entry in pubKeyCredParams was of type "public-key"',
code: "ERROR_MALFORMED_PUBKEYCREDPARAMS",
cause: e,
})
: new a({
message:
"No available authenticator supported any of the specified pubKeyCredParams algorithms",
code: "ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG",
cause: e,
});
if ("SecurityError" === e.name) {
const t = window.location.hostname;
if (!i(t))
return new a({
message: `${window.location.hostname} is an invalid domain`,
code: "ERROR_INVALID_DOMAIN",
cause: e,
});
if (r.rp.id !== t)
return new a({
message: `The RP ID "${r.rp.id}" is invalid for this domain`,
code: "ERROR_INVALID_RP_ID",
cause: e,
});
} else if ("TypeError" === e.name) {
if (r.user.id.byteLength < 1 || r.user.id.byteLength > 64)
return new a({
message: "User ID was not between 1 and 64 characters",
code: "ERROR_INVALID_USER_ID_LENGTH",
cause: e,
});
} else if ("UnknownError" === e.name)
return new a({
message:
"The authenticator was unable to process the specified options, or could not create a new credential",
code: "ERROR_AUTHENTICATOR_GENERAL_ERROR",
cause: e,
});
}
return e;
})({ error: e, options: c });
}
if (!d) throw new Error("Registration was not completed");
const { id: h, rawId: f, response: p, type: R } = d;
let w, E, g, A;
if (
("function" == typeof p.getTransports && (w = p.getTransports()),
"function" == typeof p.getPublicKeyAlgorithm)
)
try {
E = p.getPublicKeyAlgorithm();
} catch (e) {
u("getPublicKeyAlgorithm()", e);
}
if ("function" == typeof p.getPublicKey)
try {
const e = p.getPublicKey();
null !== e && (g = t(e));
} catch (e) {
u("getPublicKey()", e);
}
if ("function" == typeof p.getAuthenticatorData)
try {
A = t(p.getAuthenticatorData());
} catch (e) {
u("getAuthenticatorData()", e);
}
return {
id: h,
rawId: t(f),
response: {
attestationObject: t(p.attestationObject),
clientDataJSON: t(p.clientDataJSON),
transports: w,
publicKeyAlgorithm: E,
publicKey: g,
authenticatorData: A,
},
type: R,
clientExtensionResults: d.getClientExtensionResults(),
authenticatorAttachment: l(d.authenticatorAttachment),
};
}),
Object.defineProperty(e, "__esModule", { value: !0 });
});

110
pk-auth/script.js Normal file
View file

@ -0,0 +1,110 @@
document.getElementById("registerButton").addEventListener("click", register);
document.getElementById("loginButton").addEventListener("click", login);
function showMessage(message, isError = false) {
const messageElement = document.getElementById("message");
messageElement.textContent = message;
messageElement.style.color = isError ? "red" : "green";
}
async function register() {
// Retrieve the username from the input field
const username = document.getElementById("username").value;
try {
// Get registration options from your server. Here, we also receive the challenge.
const response = await fetch("/webauthn/passkey/registerBegin", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: username }),
});
// Check if the registration options are ok.
if (!response.ok) {
const msg = await response.json();
throw new Error(
"User already exists or failed to get registration options from server: " +
msg,
);
}
// Convert the registration options to JSON.
const options = await response.json();
console.log("registration start", options);
// This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello).
// A new attestation is created. This also means a new public-private-key pair is created.
const attestationResponse = await SimpleWebAuthnBrowser.startRegistration(
options.publicKey,
);
console.log("Attempting to complete registration", attestationResponse);
// Send attestationResponse back to server for verification and storage.
const verificationResponse = await fetch(
"/webauthn/passkey/registerFinish",
{
method: "POST",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(attestationResponse),
},
);
const msg = await verificationResponse.json();
if (verificationResponse.ok) {
showMessage(msg, false);
} else {
showMessage(msg, true);
}
} catch (error) {
showMessage("Error: " + error.message, true);
}
}
async function login() {
// Retrieve the username from the input field
const username = document.getElementById("username").value;
try {
// Get login options from your server. Here, we also receive the challenge.
const response = await fetch("/webauthn/passkey/loginBegin", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: username }),
});
// Check if the login options are ok.
if (!response.ok) {
const msg = await response.json();
throw new Error("Failed to get login options from server: " + msg);
}
// Convert the login options to JSON.
const options = await response.json();
// This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello).
// A new assertionResponse is created. This also means that the challenge has been signed.
const assertionResponse = await SimpleWebAuthnBrowser.startAuthentication(
options.publicKey,
);
// Send assertionResponse back to server for verification.
const verificationResponse = await fetch("/webauthn/passkey/loginFinish", {
method: "POST",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(assertionResponse),
});
const msg = await verificationResponse.json();
if (verificationResponse.ok) {
showMessage(msg, false);
} else {
showMessage(msg, true);
}
} catch (error) {
showMessage("Error: " + error.message, true);
}
}

65
pk-auth/test.html Normal file
View file

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passkey</title>
<link href="bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Passkey</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item active">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/private">Private</a>
</li>
</ul>
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="https://github.com/egregors/passkey" target="_blank">
<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" width="30"
height="30" alt="GitHub logo">
</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container d-flex justify-content-center align-items-center vh-100">
<div class="bg-light p-5 rounded w-50">
<h1 class="mb-4 text-center">🔑 Passkey</h1>
<div class="text-center" id="message"></div>
<div class="mb-3">
<input type="text" class="form-control" id="username" placeholder="username">
</div>
<div class="d-grid gap-2">
<div class="row">
<div class="col">
<button class="btn btn-primary w-100" id="registerButton">Register</button>
</div>
<div class="col">
<button class="btn btn-primary w-100" id="loginButton">Login</button>
</div>
</div>
</div>
</div>
</div>
<script src="index.es5.umd.min.js"></script>
<script src="script.js"></script>
</body>
</html>

1
server/auth.go Normal file
View file

@ -0,0 +1 @@
package server

53
server/middlewares.go Normal file
View file

@ -0,0 +1,53 @@
package server
import (
"context"
"net/http"
"slices"
"time"
"github.com/rs/zerolog/hlog"
"github.com/rs/zerolog/log"
)
type HandlerBuilder func(http.Handler) http.Handler
func ChainMiddlewares(base http.Handler, links ...HandlerBuilder) http.Handler {
slices.Reverse(links)
for _, f := range links {
base = f(base)
}
return base
}
func ContextValsMiddleware(pairs map[any]any) HandlerBuilder {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
for key, val := range pairs {
ctx = context.WithValue(ctx, key, val)
}
newRequest := r.WithContext(ctx)
h.ServeHTTP(w, newRequest)
})
}
}
func LoggingMiddleware(handler http.Handler) http.Handler {
return ChainMiddlewares(handler,
hlog.NewHandler(log.Logger),
hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
hlog.FromRequest(r).Info().
Str("method", r.Method).
Stringer("url", r.URL).
Int("status", status).
Int("size", size).
Dur("duration", duration).
Send()
}),
hlog.RemoteAddrHandler("ip"),
hlog.UserAgentHandler("user_agent"),
hlog.RefererHandler("referer"),
hlog.RequestIDHandler("req_id", "Request-Id"),
)
}

41
server/server.go Normal file
View file

@ -0,0 +1,41 @@
package server
import (
"fmt"
"io/fs"
"net/http"
"github.com/mstarongithub/passkey"
"github.com/rs/zerolog/log"
"gitlab.com/mstarongitlab/linstrom/storage"
)
type Server struct {
store *storage.Storage
router http.Handler
}
func NewServer(store *storage.Storage, pkey *passkey.Passkey, reactiveFS, staticFS fs.FS) *Server {
handler := buildRootHandler(pkey, reactiveFS, staticFS)
handler = ChainMiddlewares(handler, LoggingMiddleware, ContextValsMiddleware(map[any]any{}))
return &Server{
store: store,
router: handler,
}
}
func buildRootHandler(pkey *passkey.Passkey, reactiveFS, staticFS fs.FS) http.Handler {
mux := http.NewServeMux()
pkey.MountRoutes(mux, "/webauthn/")
mux.Handle("/", http.FileServerFS(reactiveFS))
mux.Handle("/nojs/", http.StripPrefix("/nojs", http.FileServerFS(staticFS)))
mux.Handle("/pk/", http.StripPrefix("/pk", http.FileServer(http.Dir("pk-auth"))))
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, true) })
return mux
}
func (s *Server) Start(addr string) {
log.Info().Str("addr", addr).Msg("Starting server")
http.ListenAndServe(addr, s.router)
}

View file

@ -7,6 +7,7 @@ import (
"time"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/google/uuid"
"github.com/mstarongithub/passkey"
"github.com/rs/zerolog/log"
"gitlab.com/mstarongitlab/linstrom/ap"
@ -197,12 +198,15 @@ func (s *Storage) NewEmptyAccount() (*Account, error) {
log.Debug().Msg("Creating new empty account")
acc := Account{}
// Generate the 64 bit id for passkey and webauthn stuff
log.Debug().Msg("Creating webauthn id for new account")
data := make([]byte, 64)
c, err := rand.Read(data)
for err != nil || c != len(data) || c < 64 {
data = make([]byte, 64)
c, err = rand.Read(data)
}
log.Debug().Msg("Random webauthn id for new account created")
acc.ID = uuid.NewString()
acc.WebAuthnId = data
acc.Followers = []string{}
acc.Tags = []string{}
@ -211,7 +215,8 @@ func (s *Storage) NewEmptyAccount() (*Account, error) {
acc.CustomFields = []uint{}
acc.IdentifiesAs = []Being{}
acc.PasskeyCredentials = []webauthn.Credential{}
res := s.db.Save(acc)
log.Debug().Any("account", &acc).Msg("Saving new account in db")
res := s.db.Save(&acc)
if res.Error != nil {
log.Error().Err(res.Error).Msg("Failed to safe new account")
return nil, res.Error

35
util/fswrapper.go Normal file
View file

@ -0,0 +1,35 @@
package util
import (
"io/fs"
"github.com/rs/zerolog/log"
)
// Fix for go:embed file systems including the full path of the embedded files
// Adds a given string to the front of all requests
type FSWrapper struct {
wrapped fs.FS
toAdd string
log bool
}
func NewFSWrapper(wraps fs.FS, appends string, logAccess bool) *FSWrapper {
return &FSWrapper{
wrapped: wraps,
toAdd: appends,
log: logAccess,
}
}
func (fs *FSWrapper) Open(name string) (fs.File, error) {
res, err := fs.wrapped.Open(fs.toAdd + name)
if fs.log {
log.Debug().
Str("prefix", fs.toAdd).
Str("filename", name).
Err(err).
Msg("fswrapper: File access result")
}
return res, err
}

21
util/zerologPasskey.go Normal file
View file

@ -0,0 +1,21 @@
package util
import "github.com/rs/zerolog/log"
type ZerologWrapper struct{}
func (z *ZerologWrapper) Errorf(format string, args ...any) {
log.Error().Msgf(format, args...)
}
func (z *ZerologWrapper) Debugf(format string, args ...any) {
log.Debug().Msgf(format, args...)
}
func (z *ZerologWrapper) Infof(format string, args ...any) {
log.Info().Msgf(format, args...)
}
func (z *ZerologWrapper) Warnf(format string, args ...any) {
log.Warn().Msgf(format, args...)
}