diff --git a/authentication-flow.md b/authentication-flow.md new file mode 100644 index 0000000..e7c4224 --- /dev/null +++ b/authentication-flow.md @@ -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 diff --git a/cmd/test/example/example.txt b/cmd/test/example/example.txt new file mode 100644 index 0000000..e69de29 diff --git a/cmd/test/main.go b/cmd/test/main.go new file mode 100644 index 0000000..554f052 --- /dev/null +++ b/cmd/test/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "embed" + "fmt" +) + +//go:embed example +var fs embed.FS + +func main() { + fmt.Println(fs.ReadDir("example")) +} diff --git a/cmd/test/test b/cmd/test/test new file mode 100755 index 0000000..fb81a2b Binary files /dev/null and b/cmd/test/test differ diff --git a/docker-compose.yml b/docker-compose.yml index 8362fbb..4c77209 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,6 @@ services: postgres: image: docker.io/postgres:16.4 - restart: unless-stopped environment: POSTGRES_PASSWORD: linstrom POSTGRES_USER: linstrom diff --git a/frontend-noscript/index.html b/frontend-noscript/test.html similarity index 100% rename from frontend-noscript/index.html rename to frontend-noscript/test.html diff --git a/frontend-reactive/.ember-cli b/frontend-reactive/.ember-cli index 4defd28..1e22a65 100644 --- a/frontend-reactive/.ember-cli +++ b/frontend-reactive/.ember-cli @@ -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 } diff --git a/frontend-reactive/package-lock.json b/frontend-reactive/package-lock.json index 5a51631..23c08fa 100644 --- a/frontend-reactive/package-lock.json +++ b/frontend-reactive/package-lock.json @@ -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", diff --git a/frontend-reactive/package.json b/frontend-reactive/package.json index 18f187d..295722e 100644 --- a/frontend-reactive/package.json +++ b/frontend-reactive/package.json @@ -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", diff --git a/go.mod b/go.mod index b6d38f1..a13db44 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index b91d024..f43807b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 677a965..e452460 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/pk-auth/index.es5.umd.min.js b/pk-auth/index.es5.umd.min.js new file mode 100644 index 0000000..98a65e2 --- /dev/null +++ b/pk-auth/index.es5.umd.min.js @@ -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 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 }); +}); diff --git a/pk-auth/script.js b/pk-auth/script.js new file mode 100644 index 0000000..d570a57 --- /dev/null +++ b/pk-auth/script.js @@ -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); + } +} diff --git a/pk-auth/test.html b/pk-auth/test.html new file mode 100644 index 0000000..3e9eda9 --- /dev/null +++ b/pk-auth/test.html @@ -0,0 +1,65 @@ + + + +
+ + +