Merge pull request #90 from ssb-ngi-pointer/sign-in-with-ssb

go side of sign-in with ssb
This commit is contained in:
Henry 2021-03-26 18:30:30 +01:00 committed by GitHub
commit 8821b99e27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 3774 additions and 330 deletions

View File

@ -26,10 +26,13 @@ import (
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
_ "github.com/mattn/go-sqlite3"
"github.com/throttled/throttled/v2"
"github.com/throttled/throttled/v2/store/memstore"
"github.com/unrolled/secure"
"go.cryptoscope.co/muxrpc/v2/debug"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/sqlite"
"github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
mksrv "github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
@ -203,10 +206,14 @@ func runroomsrv() error {
return fmt.Errorf("failed to initiate database: %w", err)
}
bridge := signinwithssb.NewSignalBridge()
// create the shs+muxrpc server
roomsrv, err := mksrv.New(
db.Members,
db.Aliases,
db.AuthWithSSB,
bridge,
httpsDomain,
opts...)
if err != nil {
@ -238,7 +245,7 @@ func runroomsrv() error {
}()
// setup web dashboard handlers
dashboardH, err := handlers.New(
webHandler, err := handlers.New(
kitlog.With(log, "package", "web"),
repo.New(repoDir),
handlers.NetworkInfo{
@ -248,9 +255,12 @@ func runroomsrv() error {
RoomID: roomsrv.Whoami(),
},
roomsrv.StateManager,
roomsrv.Network,
bridge,
handlers.Databases{
Aliases: db.Aliases,
AuthFallback: db.AuthFallback,
AuthWithSSB: db.AuthWithSSB,
DeniedKeys: db.DeniedKeys,
Invites: db.Invites,
Notices: db.Notices,
@ -283,13 +293,39 @@ func runroomsrv() error {
STSIncludeSubdomains: false,
// See for more https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
ContentSecurityPolicy: "default-src 'self'", // enforce no external content
// helpful: https://report-uri.com/home/generate
ContentSecurityPolicy: "default-src 'self'; img-src 'self' data:", // enforce no external content
BrowserXssFilter: true,
FrameDeny: true,
//ContentTypeNosniff: true, // TODO: fix Content-Type headers served from assets
})
// HTTP rate limiter
throttleStore, err := memstore.New(65536) // 64k different combinations of limitByPathAndAddr
if err != nil {
return fmt.Errorf("failed to init HTTP rate limiter store: %w", err)
}
quota := throttled.RateQuota{
MaxRate: throttled.PerSec(5), // different requests per second per VaryBy
MaxBurst: 25,
}
limiter, err := throttled.NewGCRARateLimiter(throttleStore, quota)
if err != nil {
return fmt.Errorf("failed to init HTTP rate limiter: %w", err)
}
httpRateLimiter := throttled.HTTPRateLimiter{
RateLimiter: limiter,
VaryBy: limitByPathAndAddr{},
}
// wrap dashboard/alias/invite handler in ratlimiter and security middleware
var httpHandler http.Handler
httpHandler = httpRateLimiter.RateLimit(webHandler)
httpHandler = secureMiddleware.Handler(httpHandler)
// all init was successfull
level.Info(log).Log(
"event", "serving",
"ID", roomsrv.Whoami().Ref(),
@ -305,11 +341,12 @@ func runroomsrv() error {
Addr: httpLis.Addr().String(),
// Good practice to set timeouts to avoid Slowloris attacks.
WriteTimeout: time.Second * 15,
ReadTimeout: time.Second * 15,
IdleTimeout: time.Second * 60,
// Keep in mind that the SSE stuff for "sign-in with ssb" can take a moment, thou
ReadHeaderTimeout: time.Second * 15,
WriteTimeout: time.Minute * 3,
IdleTimeout: time.Minute * 3,
Handler: secureMiddleware.Handler(dashboardH),
Handler: httpHandler,
}
err = srv.Serve(httpLis)
@ -342,3 +379,20 @@ func main() {
os.Exit(1)
}
}
type limitByPathAndAddr struct{}
func (limitByPathAndAddr) Key(r *http.Request) string {
var k strings.Builder
k.WriteString(r.URL.Path)
k.WriteString("\n")
remoteIP := r.Header.Get("X-Forwarded-For")
if remoteIP == "" {
remoteIP = r.RemoteAddr
}
k.WriteString(remoteIP)
return k.String()
}

8
go.mod
View File

@ -5,7 +5,7 @@ go 1.16
require (
github.com/BurntSushi/toml v0.3.1
github.com/PuerkitoBio/goquery v1.5.0
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/friendsofgo/errors v0.9.2
github.com/go-kit/kit v0.10.0
github.com/gofrs/uuid v4.0.0+incompatible // indirect
@ -15,13 +15,14 @@ require (
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.4.2
github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472
github.com/kevinburke/go-bindata v3.21.0+incompatible // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/nicksnyder/go-i18n/v2 v2.1.2
github.com/pkg/errors v0.9.1
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351
github.com/russross/blackfriday/v2 v2.1.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.6.1
github.com/throttled/throttled/v2 v2.7.1
github.com/unrolled/secure v1.0.8
github.com/vcraescu/go-paginator/v2 v2.0.0
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20210314195744-a1c697a68aef // indirect
@ -30,9 +31,10 @@ require (
go.cryptoscope.co/muxrpc/v2 v2.0.0-beta.1.0.20210308090127-5f1f5f9cbb59
go.cryptoscope.co/netwrap v0.1.1
go.cryptoscope.co/secretstream v1.2.2
go.mindeco.de v1.8.0
go.mindeco.de v1.9.0
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/net v0.0.0-20191116160921-f9c825593386 // indirect
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
golang.org/x/text v0.3.5

46
go.sum
View File

@ -111,6 +111,7 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
@ -145,13 +146,22 @@ github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -197,6 +207,8 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
@ -232,7 +244,6 @@ github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472 h1:6nrO82kszcc+rcKP
github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472/go.mod h1:oLLUlGld/axGHThR36o8bADQUHG+TKSUdoKqCvnoQB4=
github.com/keks/persist v0.0.0-20180731151133-9546f7b3f97e/go.mod h1:KMIOJFEE+0E/mYfYExA9vOpCFDz4TQfzk6mCOtCXR9k=
github.com/keks/persist v0.0.0-20181029214439-3af502dad70b/go.mod h1:KMIOJFEE+0E/mYfYExA9vOpCFDz4TQfzk6mCOtCXR9k=
github.com/kevinburke/go-bindata v3.21.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -298,7 +309,9 @@ github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXW
github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
@ -373,6 +386,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@ -408,6 +423,8 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/throttled/throttled/v2 v2.7.1 h1:FnBysDX4Sok55bvfDMI0l2Y71V1vM2wi7O79OW7fNtw=
github.com/throttled/throttled/v2 v2.7.1/go.mod h1:fuOeyK9fmnA+LQnsBbfT/mmPHjmkdogRBQxaD8YsgZ8=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
@ -427,18 +444,14 @@ github.com/vcraescu/go-paginator/v2 v2.0.0 h1:m9If0wF7pSjYfocrJZcyWNiWn7OfIeLFVQ
github.com/vcraescu/go-paginator/v2 v2.0.0/go.mod h1:qsrC8+/YgRL0LfurxeY3gCAtsN7oOthkIbmBdqpMX9U=
github.com/volatiletech/inflect v0.0.1 h1:2a6FcMQyhmPZcLa+uet3VJ8gLn/9svWhJxJYwvE8KsU=
github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA=
github.com/volatiletech/null/v8 v8.1.0 h1:eAO3I31A5R04usY5SKMMfDcOCnEGyT/T4wRI0JVGp4U=
github.com/volatiletech/null/v8 v8.1.0/go.mod h1:98DbwNoKEpRrYtGjWFctievIfm4n4MxG0A6EBUcoS5g=
github.com/volatiletech/null/v8 v8.1.2 h1:kiTiX1PpwvuugKwfvUNX/SU/5A2KGZMXfGD0DUHdKEI=
github.com/volatiletech/null/v8 v8.1.2/go.mod h1:98DbwNoKEpRrYtGjWFctievIfm4n4MxG0A6EBUcoS5g=
github.com/volatiletech/randomize v0.0.1 h1:eE5yajattWqTB2/eN8df4dw+8jwAzBtbdo5sbWC4nMk=
github.com/volatiletech/randomize v0.0.1/go.mod h1:GN3U0QYqfZ9FOJ67bzax1cqZ5q2xuj2mXrXBjWaRTlY=
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20200618013359-a93887c09a14 h1:2PCMsnM/GVptZVyB8s0vTIFCPjl6f5rhz5Ry5MNShMQ=
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20200618013359-a93887c09a14/go.mod h1:fmZQG/eGdD2vdjWZjrVq4v2sTQ+Alz/I09chjYWWUVw=
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20210314195744-a1c697a68aef h1:XjoYLjR/XToxGQY9O6WKA8l6t0hIWjPwazVQcQEctFM=
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20210314195744-a1c697a68aef/go.mod h1:fmZQG/eGdD2vdjWZjrVq4v2sTQ+Alz/I09chjYWWUVw=
github.com/volatiletech/sqlboiler/v4 v4.0.0/go.mod h1:U0Z5K4y+twWgHxh364G45QyzyNssSbBqNWtXGHVTlgM=
github.com/volatiletech/sqlboiler/v4 v4.4.0 h1:aSlvHidRBuxHHQZNX3ZLGgzNVPVPzWqsC3lhcLbV/b0=
github.com/volatiletech/sqlboiler/v4 v4.4.0/go.mod h1:h4RBAO6QbwMP3ezGmtfGljRms7S27cFIgF3rKgPKstE=
github.com/volatiletech/sqlboiler/v4 v4.5.0 h1:oJ3YXEvv0c48S9W/3TuPLxJxefIkewpub2qZioXXlUY=
github.com/volatiletech/sqlboiler/v4 v4.5.0/go.mod h1:tQgF5zxwqrjR6Wydc5rRylI6puDOO1WvBC70/5up+Hg=
github.com/volatiletech/strmangle v0.0.1 h1:UKQoHmY6be/R3tSvD2nQYrH41k43OJkidwEiC74KIzk=
@ -461,8 +474,6 @@ go.cryptoscope.co/margaret v0.0.5/go.mod h1:W+Q6lvzHIrF8+Yt3dItHHsx2R9/Xvj/NkJGM
go.cryptoscope.co/margaret v0.0.8/go.mod h1:VbP0bqavqW5osTmdNvgukrqtmqvZC5sXyPSBUW5rzv8=
go.cryptoscope.co/margaret v0.0.12-0.20190912103626-34323ad497f4 h1:gLSldWRujtUOfdnpA1XKD71xcCp3Wz1URMnT6xpUPV4=
go.cryptoscope.co/margaret v0.0.12-0.20190912103626-34323ad497f4/go.mod h1:3rt+RmZTFZEgfvFxz0ZPDBIWtLJOouWtzV6YbBl6sek=
go.cryptoscope.co/muxrpc/v2 v2.0.0-20210202162901-fe642d405dc6 h1:p135TwijE3DbmklGygc7++MMRRVlujmjqed8kEOmwLs=
go.cryptoscope.co/muxrpc/v2 v2.0.0-20210202162901-fe642d405dc6/go.mod h1:MgaeojIkWY3lLuoNw1mlMT3b3jiZwOj/fgsoGZp/VNA=
go.cryptoscope.co/muxrpc/v2 v2.0.0-beta.1.0.20210308090127-5f1f5f9cbb59 h1:Gv5pKkvHYJNc12uRZ/jMCsR17G7v6oFLLCrGAUVxhvo=
go.cryptoscope.co/muxrpc/v2 v2.0.0-beta.1.0.20210308090127-5f1f5f9cbb59/go.mod h1:MgaeojIkWY3lLuoNw1mlMT3b3jiZwOj/fgsoGZp/VNA=
go.cryptoscope.co/netwrap v0.1.0/go.mod h1:7zcYswCa4CT+ct54e9uH9+IIbYYETEMHKDNpzl8Ukew=
@ -473,8 +484,8 @@ go.cryptoscope.co/secretstream v1.2.2/go.mod h1:7nRGZ7fTqSgQAnv2Y4m8xQsS3MFxvB7I
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.mindeco.de v1.8.0 h1:Vxob3XaDz85aD4wq8VbQxtradpHbmjciG2eSJLGaFV0=
go.mindeco.de v1.8.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
go.mindeco.de v1.9.0 h1:/xli02DkzpIUZxp/rp1nj8z/OZ9MHvkMIr9TfDVcmBg=
go.mindeco.de v1.9.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870 h1:TCI3AefMAaOYECvppn30+CfEB0Fn8IES1SKvvacc3/c=
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870/go.mod h1:OnBnV02ux4lLsZ39LID6yYLqSDp+dqTHb/3miYPkQFs=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
@ -516,8 +527,10 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191116160921-f9c825593386 h1:ktbWvQrW08Txdxno1PiDpSxPXG6ndGsfnJjRRtkM0LQ=
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -549,6 +562,7 @@ golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191007154456-ef33b2fb2c41/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -599,10 +613,17 @@ google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
@ -620,6 +641,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.7/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=

View File

@ -24,13 +24,22 @@ type EndpointStat struct {
Endpoint muxrpc.Endpoint
}
//go:generate counterfeiter -o mocked/endpoints.go . Endpoints
// Endpoints returns the connected endpoint for the passed feed,
// or false if there is none.
type Endpoints interface {
GetEndpointFor(refs.FeedRef) (muxrpc.Endpoint, bool)
}
// Network supplies all network related functionalitiy
type Network interface {
Connect(ctx context.Context, addr net.Addr) error
Serve(context.Context, ...muxrpc.HandlerWrapper) error
GetListenAddr() net.Addr
GetAllEndpoints() []EndpointStat
GetEndpointFor(refs.FeedRef) (muxrpc.Endpoint, bool)
Endpoints
GetConnTracker() ConnTracker

View File

@ -0,0 +1,118 @@
// Code generated by counterfeiter. DO NOT EDIT.
package mocked
import (
"sync"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
muxrpc "go.cryptoscope.co/muxrpc/v2"
refs "go.mindeco.de/ssb-refs"
)
type FakeEndpoints struct {
GetEndpointForStub func(refs.FeedRef) (muxrpc.Endpoint, bool)
getEndpointForMutex sync.RWMutex
getEndpointForArgsForCall []struct {
arg1 refs.FeedRef
}
getEndpointForReturns struct {
result1 muxrpc.Endpoint
result2 bool
}
getEndpointForReturnsOnCall map[int]struct {
result1 muxrpc.Endpoint
result2 bool
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeEndpoints) GetEndpointFor(arg1 refs.FeedRef) (muxrpc.Endpoint, bool) {
fake.getEndpointForMutex.Lock()
ret, specificReturn := fake.getEndpointForReturnsOnCall[len(fake.getEndpointForArgsForCall)]
fake.getEndpointForArgsForCall = append(fake.getEndpointForArgsForCall, struct {
arg1 refs.FeedRef
}{arg1})
stub := fake.GetEndpointForStub
fakeReturns := fake.getEndpointForReturns
fake.recordInvocation("GetEndpointFor", []interface{}{arg1})
fake.getEndpointForMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeEndpoints) GetEndpointForCallCount() int {
fake.getEndpointForMutex.RLock()
defer fake.getEndpointForMutex.RUnlock()
return len(fake.getEndpointForArgsForCall)
}
func (fake *FakeEndpoints) GetEndpointForCalls(stub func(refs.FeedRef) (muxrpc.Endpoint, bool)) {
fake.getEndpointForMutex.Lock()
defer fake.getEndpointForMutex.Unlock()
fake.GetEndpointForStub = stub
}
func (fake *FakeEndpoints) GetEndpointForArgsForCall(i int) refs.FeedRef {
fake.getEndpointForMutex.RLock()
defer fake.getEndpointForMutex.RUnlock()
argsForCall := fake.getEndpointForArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeEndpoints) GetEndpointForReturns(result1 muxrpc.Endpoint, result2 bool) {
fake.getEndpointForMutex.Lock()
defer fake.getEndpointForMutex.Unlock()
fake.GetEndpointForStub = nil
fake.getEndpointForReturns = struct {
result1 muxrpc.Endpoint
result2 bool
}{result1, result2}
}
func (fake *FakeEndpoints) GetEndpointForReturnsOnCall(i int, result1 muxrpc.Endpoint, result2 bool) {
fake.getEndpointForMutex.Lock()
defer fake.getEndpointForMutex.Unlock()
fake.GetEndpointForStub = nil
if fake.getEndpointForReturnsOnCall == nil {
fake.getEndpointForReturnsOnCall = make(map[int]struct {
result1 muxrpc.Endpoint
result2 bool
})
}
fake.getEndpointForReturnsOnCall[i] = struct {
result1 muxrpc.Endpoint
result2 bool
}{result1, result2}
}
func (fake *FakeEndpoints) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.getEndpointForMutex.RLock()
defer fake.getEndpointForMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeEndpoints) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ network.Endpoints = new(FakeEndpoints)

View File

@ -0,0 +1,16 @@
package randutil
import "math/rand"
var alphabet = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
// String returns a random string of length n, using the alphnum character set (a-z, A-Z, 0-9)
func String(n int) string {
s := make([]rune, n)
for i := range s {
s[i] = alphabet[rand.Intn(len(alphabet))]
}
return string(s)
}

View File

@ -0,0 +1,122 @@
// SPDX-License-Identifier: MIT
package signinwithssb
import (
"fmt"
"sync"
"time"
)
// SignalBridge implements a way for muxrpc and http handlers to communicate about SIWSSB events
type SignalBridge struct {
mu *sync.Mutex
sessions sessionMap
}
type sessionMap map[string]chan Event
// Event is the unit of information that is sent over the bridge.
type Event struct {
Worked bool
// the token value if it did work
Token string
// reason why it didn't work
Reason error
}
// NewSignalBridge returns a new SignalBridge
func NewSignalBridge() *SignalBridge {
return &SignalBridge{
mu: new(sync.Mutex),
sessions: make(sessionMap),
}
}
// RegisterSession registers a new session on the bridge.
// It returns a fresh server challenge, which acts as the session key.
func (sb *SignalBridge) RegisterSession() string {
sb.mu.Lock()
defer sb.mu.Unlock()
c := GenerateChallenge()
_, used := sb.sessions[c]
if used {
for used { // generate new challenges until we have an un-used one
c = GenerateChallenge()
_, used = sb.sessions[c]
}
}
evtCh := make(chan Event)
sb.sessions[c] = evtCh
go func() { // make sure the session doesn't go stale and collect dust (ie unused memory)
time.Sleep(10 * time.Minute)
sb.mu.Lock()
defer sb.mu.Unlock()
delete(sb.sessions, c)
}()
return c
}
// GetEventChannel returns the channel for the passed challenge from which future events can be read.
// If sc doesn't exist, the 2nd argument is false.
func (sb *SignalBridge) GetEventChannel(sc string) (<-chan Event, bool) {
sb.mu.Lock()
defer sb.mu.Unlock()
ch, has := sb.sessions[sc]
return ch, has
}
// SessionWorked uses the passed challenge to send on and close the open channel.
// It will return an error if the session doesn't exist.
func (sb *SignalBridge) SessionWorked(sc string, token string) error {
return sb.sendAndClose(sc, Event{
Worked: true,
Token: token,
})
}
// SessionFailed uses the passed challenge to send on and close the open channel.
// It will return an error if the session doesn't exist.
func (sb *SignalBridge) SessionFailed(sc string, reason error) error {
return sb.sendAndClose(sc, Event{
Worked: false,
Reason: reason,
})
}
func (sb *SignalBridge) sendAndClose(sc string, evt Event) error {
sb.mu.Lock()
defer sb.mu.Unlock()
ch, ok := sb.sessions[sc]
if !ok {
return fmt.Errorf("no such session")
}
var (
err error
timeout = time.NewTimer(2 * time.Minute)
)
// handle what happens if the sse client isn't connected
select {
case <-timeout.C:
err = fmt.Errorf("faled to send completed session")
case ch <- evt:
timeout.Stop()
}
// session is finalized either way
close(ch)
delete(sb.sessions, sc)
return err
}

View File

@ -0,0 +1,88 @@
// SPDX-License-Identifier: MIT
package signinwithssb
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestBridgeWorked(t *testing.T) {
t.Parallel()
a := assert.New(t)
sb := NewSignalBridge()
// try to use a non-existant session
err := sb.SessionWorked("nope", "just a test")
a.Error(err)
// make a new session
sc := sb.RegisterSession()
b, err := DecodeChallengeString(sc)
a.NoError(err)
a.Len(b, challengeLength)
updates, has := sb.GetEventChannel(sc)
a.True(has)
go func() {
err := sb.SessionWorked(sc, "a token")
a.NoError(err)
}()
time.Sleep(time.Second / 4)
select {
case evt := <-updates:
a.True(evt.Worked)
a.Equal("a token", evt.Token)
a.Nil(evt.Reason)
default:
t.Error("no updates")
}
}
func TestBridgeFailed(t *testing.T) {
t.Parallel()
a := assert.New(t)
sb := NewSignalBridge()
// try to use a non-existant session
testReason := fmt.Errorf("just an error")
err := sb.SessionFailed("nope", testReason)
a.Error(err)
// make a new session
sc := sb.RegisterSession()
b, err := DecodeChallengeString(sc)
a.NoError(err)
a.Len(b, challengeLength)
updates, has := sb.GetEventChannel(sc)
a.True(has)
go func() {
err := sb.SessionFailed(sc, testReason)
a.NoError(err)
}()
time.Sleep(time.Second / 4)
select {
case evt := <-updates:
a.False(evt.Worked)
a.Equal("", evt.Token)
a.EqualError(testReason, evt.Reason.Error())
default:
t.Error("no updates")
}
}

View File

@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT
package signinwithssb
import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"golang.org/x/crypto/ed25519"
refs "go.mindeco.de/ssb-refs"
)
// sign-in with ssb uses 256-bit nonces
const challengeLength = 32
// DecodeChallengeString accepts base64 encoded strings and decodes them,
// checks their length to be equal to challengeLength,
// and returns the decoded bytes
func DecodeChallengeString(c string) ([]byte, error) {
challengeBytes, err := base64.URLEncoding.DecodeString(c)
if err != nil {
return nil, fmt.Errorf("invalid challenge encoding: %w", err)
}
if n := len(challengeBytes); n != challengeLength {
return nil, fmt.Errorf("invalid challenge length: expected %d but got %d", challengeLength, n)
}
return challengeBytes, nil
}
// GenerateChallenge returs a base64 encoded string
// with challangeLength bytes of random data
func GenerateChallenge() string {
buf := make([]byte, challengeLength)
rand.Read(buf)
return base64.URLEncoding.EncodeToString(buf)
}
// ClientPayload is used to create and verify solutions
type ClientPayload struct {
ClientID, ServerID refs.FeedRef
ClientChallenge string
ServerChallenge string
}
// recreate the signed message
func (cr ClientPayload) createMessage() []byte {
var msg bytes.Buffer
msg.WriteString("=http-auth-sign-in:")
msg.WriteString(cr.ServerID.Ref())
msg.WriteString(":")
msg.WriteString(cr.ClientID.Ref())
msg.WriteString(":")
msg.WriteString(cr.ServerChallenge)
msg.WriteString(":")
msg.WriteString(cr.ClientChallenge)
return msg.Bytes()
}
// Sign returns the signature created with the passed privateKey
func (cr ClientPayload) Sign(privateKey ed25519.PrivateKey) []byte {
msg := cr.createMessage()
return ed25519.Sign(privateKey, msg)
}
// Validate checks the signature by calling createMessage() and ed25519.Verify()
// together with the ClientID public key.
func (cr ClientPayload) Validate(signature []byte) bool {
msg := cr.createMessage()
return ed25519.Verify(cr.ClientID.PubKey(), msg, signature)
}

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
package signinwithssb
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
refs "go.mindeco.de/ssb-refs"
)
func TestPayloadString(t *testing.T) {
server := refs.FeedRef{ID: bytes.Repeat([]byte{1}, 32), Algo: "test"}
client := refs.FeedRef{ID: bytes.Repeat([]byte{2}, 32), Algo: "test"}
var req ClientPayload
req.ServerID = server
req.ClientID = client
req.ServerChallenge = "fooo"
req.ClientChallenge = "barr"
want := "=http-auth-sign-in:@AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=.test:@AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=.test:fooo:barr"
got := req.createMessage()
assert.Equal(t, want, string(got))
}
func TestGenerateAndDecode(t *testing.T) {
r := require.New(t)
b, err := DecodeChallengeString(GenerateChallenge())
r.NoError(err)
r.Len(b, challengeLength)
b, err = DecodeChallengeString("toshort")
r.Error(err)
r.Nil(b)
}

View File

@ -0,0 +1,103 @@
// SPDX-License-Identifier: MIT
package gossip
import (
"context"
"encoding/json"
"fmt"
"io"
"time"
"go.cryptoscope.co/muxrpc/v2"
"go.mindeco.de/encodedTime"
)
// Ping implements the server side of gossip.ping.
// it's idea is mentioned here https://github.com/ssbc/ssb-gossip/#ping-duplex
// and implemented by https://github.com/dominictarr/pull-ping/
//
func Ping(ctx context.Context, req *muxrpc.Request, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error {
type arg struct {
Timeout int
}
var args []arg
err := json.Unmarshal(req.RawArgs, &args)
if err != nil {
return err
}
// var timeout = time.Minute * 5
// if len(args) == 1 {
// timeout = time.Minute * time.Duration(args[0].Timeout/(60*1000))
// }
go func() {
peerSnk.SetEncoding(muxrpc.TypeJSON)
enc := json.NewEncoder(peerSnk)
tick := time.NewTicker(5 * time.Second)
defer tick.Stop()
for {
select {
case <-ctx.Done():
return
case <-tick.C:
}
var pong = encodedTime.Millisecs(time.Now())
err = enc.Encode(pong)
if err != nil {
return
}
}
}()
for peerSrc.Next(ctx) {
var ping encodedTime.Millisecs
err := peerSrc.Reader(func(rd io.Reader) error {
return json.NewDecoder(rd).Decode(&ping)
})
if err != nil {
return err
}
// when := time.Time(ping)
// fmt.Printf("got ping: %s - age: %s\n", when.String(), time.Since(when))
}
return nil
}
// this is how it should work, i think, but it leads to disconnects...
// From the code it's hard to see but the client sends a timestamp in milliseconds (Date.now() in javascript/json)
// and the other side responds with it's own timestamp.
func actualPingPong(ctx context.Context, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error {
peerSnk.SetEncoding(muxrpc.TypeJSON)
enc := json.NewEncoder(peerSnk)
for peerSrc.Next(ctx) {
var ping encodedTime.Millisecs
err := peerSrc.Reader(func(rd io.Reader) error {
return json.NewDecoder(rd).Decode(&ping)
})
if err != nil {
return err
}
when := time.Time(ping)
fmt.Printf("got ping: %s - age: %s\n", when.String(), time.Since(when))
pong := encodedTime.Millisecs(time.Now())
err = enc.Encode(pong)
if err != nil {
return err
}
// time.Sleep(timeout)
}
return peerSrc.Err()
}

View File

@ -0,0 +1,133 @@
// SPDX-License-Identifier: MIT
package signinwithssb
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
kitlog "github.com/go-kit/kit/log"
"go.cryptoscope.co/muxrpc/v2"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
validate "github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
refs "go.mindeco.de/ssb-refs"
)
// Handler implements the muxrpc methods for the "Sign-in with SSB" calls. SendSolution and InvalidateAllSolutions.
type Handler struct {
logger kitlog.Logger
self refs.FeedRef
sessions roomdb.AuthWithSSBService
members roomdb.MembersService
bridge *signinwithssb.SignalBridge
roomDomain string // the http(s) domain of the room to signal redirect addresses
}
// New returns the muxrpc handler for Sign-in with SSB
func New(
log kitlog.Logger,
self refs.FeedRef,
roomDomain string,
membersdb roomdb.MembersService,
sessiondb roomdb.AuthWithSSBService,
bridge *signinwithssb.SignalBridge,
) Handler {
var h Handler
h.self = self
h.roomDomain = roomDomain
h.logger = log
h.sessions = sessiondb
h.members = membersdb
h.bridge = bridge
return h
}
// SendSolution implements the receiving end of httpAuth.sendSolution.
// It recevies three parameters [sc, cc, sol], does the validation and if it passes creates a token
// and signals the created token to the SSE HTTP handler using the signal bridge.
func (h Handler) SendSolution(ctx context.Context, req *muxrpc.Request) (interface{}, error) {
clientID, err := network.GetFeedRefFromAddr(req.RemoteAddr())
if err != nil {
return nil, err
}
member, err := h.members.GetByFeed(ctx, *clientID)
if err != nil {
return nil, fmt.Errorf("client is not a room member")
}
var params []string
if err := json.Unmarshal(req.RawArgs, &params); err != nil {
return nil, err
}
if n := len(params); n != 3 {
return nil, fmt.Errorf("expected 3 arguments (sc, cc, sol) but got %d", n)
}
var payload validate.ClientPayload
payload.ServerID = h.self
payload.ServerChallenge = params[0]
payload.ClientID = *clientID
payload.ClientChallenge = params[1]
sig, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(params[2], ".sig.ed25519"))
if err != nil {
h.bridge.SessionFailed(payload.ServerChallenge, err)
return nil, fmt.Errorf("signature is not valid base64 data: %w", err)
}
if !payload.Validate(sig) {
err = fmt.Errorf("not a valid solution")
h.bridge.SessionFailed(payload.ServerChallenge, err)
return nil, err
}
tok, err := h.sessions.CreateToken(ctx, member.ID)
if err != nil {
h.bridge.SessionFailed(payload.ServerChallenge, err)
return nil, err
}
err = h.bridge.SessionWorked(payload.ServerChallenge, tok)
if err != nil {
h.sessions.RemoveToken(ctx, tok)
return nil, err
}
return true, nil
}
// InvalidateAllSolutions implements the muxrpc call httpAuth.invalidateAllSolutions
func (h Handler) InvalidateAllSolutions(ctx context.Context, req *muxrpc.Request) (interface{}, error) {
// get the feed from the muxrpc connection
clientID, err := network.GetFeedRefFromAddr(req.RemoteAddr())
if err != nil {
return nil, err
}
// lookup the member
member, err := h.members.GetByFeed(ctx, *clientID)
if err != nil {
return nil, err
}
// delete all SIWSSB sessions of that member
err = h.sessions.WipeTokensForMember(ctx, member.ID)
if err != nil {
return nil, err
}
return true, nil
}

View File

@ -10,6 +10,8 @@ import (
"testing"
"time"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.cryptoscope.co/muxrpc/v2"
@ -106,7 +108,10 @@ func TestAliasRegister(t *testing.T) {
r.NoError(err)
t.Log("got URL:", resolveURL)
a.Equal("srv", resolveURL.Host)
a.Equal("/bob", resolveURL.Path)
wantURL, err := router.CompleteApp().Get(router.CompleteAliasResolve).URL("alias", "bob")
r.NoError(err)
a.Equal(wantURL.Path, resolveURL.Path)
// server should have the alias now
alias, err := serv.Aliases.Resolve(ctx, "bob")

View File

@ -23,6 +23,7 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/testutils"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/sqlite"
"github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
)
@ -88,7 +89,8 @@ func makeNamedTestBot(t testing.TB, name string, opts []roomsrv.Option) (roomdb.
t.Log("db close failed: ", err)
}
})
theBot, err := roomsrv.New(db.Members, db.Aliases, name, botOptions...)
sb := signinwithssb.NewSignalBridge()
theBot, err := roomsrv.New(db.Members, db.Aliases, db.AuthWithSSB, sb, name, botOptions...)
r.NoError(err)
return db.Members, theBot
}

View File

@ -28,7 +28,9 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/testutils"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
refs "go.mindeco.de/ssb-refs"
)
@ -117,7 +119,11 @@ func (ts *testSession) startGoServer(
}),
)
srv, err := roomsrv.New(membersDB, aliasDB, "go.test.room.server", opts...)
// not needed for testing yet
sb := signinwithssb.NewSignalBridge()
authSessionsDB := new(mockdb.FakeAuthWithSSBService)
srv, err := roomsrv.New(membersDB, aliasDB, authSessionsDB, sb, "go.test.room.server", opts...)
r.NoError(err, "failed to init tees a server")
ts.t.Logf("go server: %s", srv.Whoami().Ref())
ts.t.Cleanup(func() {

View File

@ -32,9 +32,24 @@ type AuthFallbackService interface {
// Remove(pwid)
}
// needed?! not sure we need to hold the challanges
// AuthWithSSBService defines functions needed for the challange/response system of sign-in with ssb
type AuthWithSSBService interface{}
// AuthWithSSBService defines utility functions for the challenge/response system of sign-in with ssb
// They are particualarly of service to check valid sessions (after the client provided a solution for a challenge)
// And to log out valid sessions from the clients device.
type AuthWithSSBService interface {
// CreateToken is used to generate a token that is stored inside a cookie.
// It is used after a valid solution for a challenge was provided.
CreateToken(ctx context.Context, memberID int64) (string, error)
// CheckToken checks if the passed token is still valid and returns the member id if so
CheckToken(ctx context.Context, token string) (int64, error)
// RemoveToken removes a single token from the database
RemoveToken(ctx context.Context, token string) error
// WipeTokensForMember deletes all tokens currently held for that member
WipeTokensForMember(ctx context.Context, memberID int64) error
}
// MembersService stores and retreives the list of internal users (members, mods and admins).
type MembersService interface {

View File

@ -2,19 +2,334 @@
package mockdb
import (
"context"
"sync"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
)
type FakeAuthWithSSBService struct {
CheckTokenStub func(context.Context, string) (int64, error)
checkTokenMutex sync.RWMutex
checkTokenArgsForCall []struct {
arg1 context.Context
arg2 string
}
checkTokenReturns struct {
result1 int64
result2 error
}
checkTokenReturnsOnCall map[int]struct {
result1 int64
result2 error
}
CreateTokenStub func(context.Context, int64) (string, error)
createTokenMutex sync.RWMutex
createTokenArgsForCall []struct {
arg1 context.Context
arg2 int64
}
createTokenReturns struct {
result1 string
result2 error
}
createTokenReturnsOnCall map[int]struct {
result1 string
result2 error
}
RemoveTokenStub func(context.Context, string) error
removeTokenMutex sync.RWMutex
removeTokenArgsForCall []struct {
arg1 context.Context
arg2 string
}
removeTokenReturns struct {
result1 error
}
removeTokenReturnsOnCall map[int]struct {
result1 error
}
WipeTokensForMemberStub func(context.Context, int64) error
wipeTokensForMemberMutex sync.RWMutex
wipeTokensForMemberArgsForCall []struct {
arg1 context.Context
arg2 int64
}
wipeTokensForMemberReturns struct {
result1 error
}
wipeTokensForMemberReturnsOnCall map[int]struct {
result1 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeAuthWithSSBService) CheckToken(arg1 context.Context, arg2 string) (int64, error) {
fake.checkTokenMutex.Lock()
ret, specificReturn := fake.checkTokenReturnsOnCall[len(fake.checkTokenArgsForCall)]
fake.checkTokenArgsForCall = append(fake.checkTokenArgsForCall, struct {
arg1 context.Context
arg2 string
}{arg1, arg2})
stub := fake.CheckTokenStub
fakeReturns := fake.checkTokenReturns
fake.recordInvocation("CheckToken", []interface{}{arg1, arg2})
fake.checkTokenMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeAuthWithSSBService) CheckTokenCallCount() int {
fake.checkTokenMutex.RLock()
defer fake.checkTokenMutex.RUnlock()
return len(fake.checkTokenArgsForCall)
}
func (fake *FakeAuthWithSSBService) CheckTokenCalls(stub func(context.Context, string) (int64, error)) {
fake.checkTokenMutex.Lock()
defer fake.checkTokenMutex.Unlock()
fake.CheckTokenStub = stub
}
func (fake *FakeAuthWithSSBService) CheckTokenArgsForCall(i int) (context.Context, string) {
fake.checkTokenMutex.RLock()
defer fake.checkTokenMutex.RUnlock()
argsForCall := fake.checkTokenArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeAuthWithSSBService) CheckTokenReturns(result1 int64, result2 error) {
fake.checkTokenMutex.Lock()
defer fake.checkTokenMutex.Unlock()
fake.CheckTokenStub = nil
fake.checkTokenReturns = struct {
result1 int64
result2 error
}{result1, result2}
}
func (fake *FakeAuthWithSSBService) CheckTokenReturnsOnCall(i int, result1 int64, result2 error) {
fake.checkTokenMutex.Lock()
defer fake.checkTokenMutex.Unlock()
fake.CheckTokenStub = nil
if fake.checkTokenReturnsOnCall == nil {
fake.checkTokenReturnsOnCall = make(map[int]struct {
result1 int64
result2 error
})
}
fake.checkTokenReturnsOnCall[i] = struct {
result1 int64
result2 error
}{result1, result2}
}
func (fake *FakeAuthWithSSBService) CreateToken(arg1 context.Context, arg2 int64) (string, error) {
fake.createTokenMutex.Lock()
ret, specificReturn := fake.createTokenReturnsOnCall[len(fake.createTokenArgsForCall)]
fake.createTokenArgsForCall = append(fake.createTokenArgsForCall, struct {
arg1 context.Context
arg2 int64
}{arg1, arg2})
stub := fake.CreateTokenStub
fakeReturns := fake.createTokenReturns
fake.recordInvocation("CreateToken", []interface{}{arg1, arg2})
fake.createTokenMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeAuthWithSSBService) CreateTokenCallCount() int {
fake.createTokenMutex.RLock()
defer fake.createTokenMutex.RUnlock()
return len(fake.createTokenArgsForCall)
}
func (fake *FakeAuthWithSSBService) CreateTokenCalls(stub func(context.Context, int64) (string, error)) {
fake.createTokenMutex.Lock()
defer fake.createTokenMutex.Unlock()
fake.CreateTokenStub = stub
}
func (fake *FakeAuthWithSSBService) CreateTokenArgsForCall(i int) (context.Context, int64) {
fake.createTokenMutex.RLock()
defer fake.createTokenMutex.RUnlock()
argsForCall := fake.createTokenArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeAuthWithSSBService) CreateTokenReturns(result1 string, result2 error) {
fake.createTokenMutex.Lock()
defer fake.createTokenMutex.Unlock()
fake.CreateTokenStub = nil
fake.createTokenReturns = struct {
result1 string
result2 error
}{result1, result2}
}
func (fake *FakeAuthWithSSBService) CreateTokenReturnsOnCall(i int, result1 string, result2 error) {
fake.createTokenMutex.Lock()
defer fake.createTokenMutex.Unlock()
fake.CreateTokenStub = nil
if fake.createTokenReturnsOnCall == nil {
fake.createTokenReturnsOnCall = make(map[int]struct {
result1 string
result2 error
})
}
fake.createTokenReturnsOnCall[i] = struct {
result1 string
result2 error
}{result1, result2}
}
func (fake *FakeAuthWithSSBService) RemoveToken(arg1 context.Context, arg2 string) error {
fake.removeTokenMutex.Lock()
ret, specificReturn := fake.removeTokenReturnsOnCall[len(fake.removeTokenArgsForCall)]
fake.removeTokenArgsForCall = append(fake.removeTokenArgsForCall, struct {
arg1 context.Context
arg2 string
}{arg1, arg2})
stub := fake.RemoveTokenStub
fakeReturns := fake.removeTokenReturns
fake.recordInvocation("RemoveToken", []interface{}{arg1, arg2})
fake.removeTokenMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeAuthWithSSBService) RemoveTokenCallCount() int {
fake.removeTokenMutex.RLock()
defer fake.removeTokenMutex.RUnlock()
return len(fake.removeTokenArgsForCall)
}
func (fake *FakeAuthWithSSBService) RemoveTokenCalls(stub func(context.Context, string) error) {
fake.removeTokenMutex.Lock()
defer fake.removeTokenMutex.Unlock()
fake.RemoveTokenStub = stub
}
func (fake *FakeAuthWithSSBService) RemoveTokenArgsForCall(i int) (context.Context, string) {
fake.removeTokenMutex.RLock()
defer fake.removeTokenMutex.RUnlock()
argsForCall := fake.removeTokenArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeAuthWithSSBService) RemoveTokenReturns(result1 error) {
fake.removeTokenMutex.Lock()
defer fake.removeTokenMutex.Unlock()
fake.RemoveTokenStub = nil
fake.removeTokenReturns = struct {
result1 error
}{result1}
}
func (fake *FakeAuthWithSSBService) RemoveTokenReturnsOnCall(i int, result1 error) {
fake.removeTokenMutex.Lock()
defer fake.removeTokenMutex.Unlock()
fake.RemoveTokenStub = nil
if fake.removeTokenReturnsOnCall == nil {
fake.removeTokenReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.removeTokenReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeAuthWithSSBService) WipeTokensForMember(arg1 context.Context, arg2 int64) error {
fake.wipeTokensForMemberMutex.Lock()
ret, specificReturn := fake.wipeTokensForMemberReturnsOnCall[len(fake.wipeTokensForMemberArgsForCall)]
fake.wipeTokensForMemberArgsForCall = append(fake.wipeTokensForMemberArgsForCall, struct {
arg1 context.Context
arg2 int64
}{arg1, arg2})
stub := fake.WipeTokensForMemberStub
fakeReturns := fake.wipeTokensForMemberReturns
fake.recordInvocation("WipeTokensForMember", []interface{}{arg1, arg2})
fake.wipeTokensForMemberMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeAuthWithSSBService) WipeTokensForMemberCallCount() int {
fake.wipeTokensForMemberMutex.RLock()
defer fake.wipeTokensForMemberMutex.RUnlock()
return len(fake.wipeTokensForMemberArgsForCall)
}
func (fake *FakeAuthWithSSBService) WipeTokensForMemberCalls(stub func(context.Context, int64) error) {
fake.wipeTokensForMemberMutex.Lock()
defer fake.wipeTokensForMemberMutex.Unlock()
fake.WipeTokensForMemberStub = stub
}
func (fake *FakeAuthWithSSBService) WipeTokensForMemberArgsForCall(i int) (context.Context, int64) {
fake.wipeTokensForMemberMutex.RLock()
defer fake.wipeTokensForMemberMutex.RUnlock()
argsForCall := fake.wipeTokensForMemberArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeAuthWithSSBService) WipeTokensForMemberReturns(result1 error) {
fake.wipeTokensForMemberMutex.Lock()
defer fake.wipeTokensForMemberMutex.Unlock()
fake.WipeTokensForMemberStub = nil
fake.wipeTokensForMemberReturns = struct {
result1 error
}{result1}
}
func (fake *FakeAuthWithSSBService) WipeTokensForMemberReturnsOnCall(i int, result1 error) {
fake.wipeTokensForMemberMutex.Lock()
defer fake.wipeTokensForMemberMutex.Unlock()
fake.WipeTokensForMemberStub = nil
if fake.wipeTokensForMemberReturnsOnCall == nil {
fake.wipeTokensForMemberReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.wipeTokensForMemberReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeAuthWithSSBService) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.checkTokenMutex.RLock()
defer fake.checkTokenMutex.RUnlock()
fake.createTokenMutex.RLock()
defer fake.createTokenMutex.RUnlock()
fake.removeTokenMutex.RLock()
defer fake.removeTokenMutex.RUnlock()
fake.wipeTokensForMemberMutex.RLock()
defer fake.wipeTokensForMemberMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value

View File

@ -26,19 +26,6 @@ type FakeMembersService struct {
result1 int64
result2 error
}
ChangeRoleStub func(context.Context, int64, roomdb.Role) error
changeRoleMutex sync.RWMutex
changeRoleArgsForCall []struct {
arg1 context.Context
arg2 int64
arg3 roomdb.Role
}
changeRoleReturns struct {
result1 error
}
changeRoleReturnsOnCall map[int]struct {
result1 error
}
GetByFeedStub func(context.Context, refs.FeedRef) (roomdb.Member, error)
getByFeedMutex sync.RWMutex
getByFeedArgsForCall []struct {
@ -188,69 +175,6 @@ func (fake *FakeMembersService) AddReturnsOnCall(i int, result1 int64, result2 e
}{result1, result2}
}
func (fake *FakeMembersService) ChangeRole(arg1 context.Context, arg2 int64, arg3 roomdb.Role) error {
fake.changeRoleMutex.Lock()
ret, specificReturn := fake.changeRoleReturnsOnCall[len(fake.changeRoleArgsForCall)]
fake.changeRoleArgsForCall = append(fake.changeRoleArgsForCall, struct {
arg1 context.Context
arg2 int64
arg3 roomdb.Role
}{arg1, arg2, arg3})
stub := fake.ChangeRoleStub
fakeReturns := fake.changeRoleReturns
fake.recordInvocation("ChangeRole", []interface{}{arg1, arg2, arg3})
fake.changeRoleMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeMembersService) ChangeRoleCallCount() int {
fake.changeRoleMutex.RLock()
defer fake.changeRoleMutex.RUnlock()
return len(fake.changeRoleArgsForCall)
}
func (fake *FakeMembersService) ChangeRoleCalls(stub func(context.Context, int64, roomdb.Role) error) {
fake.changeRoleMutex.Lock()
defer fake.changeRoleMutex.Unlock()
fake.ChangeRoleStub = stub
}
func (fake *FakeMembersService) ChangeRoleArgsForCall(i int) (context.Context, int64, roomdb.Role) {
fake.changeRoleMutex.RLock()
defer fake.changeRoleMutex.RUnlock()
argsForCall := fake.changeRoleArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeMembersService) ChangeRoleReturns(result1 error) {
fake.changeRoleMutex.Lock()
defer fake.changeRoleMutex.Unlock()
fake.ChangeRoleStub = nil
fake.changeRoleReturns = struct {
result1 error
}{result1}
}
func (fake *FakeMembersService) ChangeRoleReturnsOnCall(i int, result1 error) {
fake.changeRoleMutex.Lock()
defer fake.changeRoleMutex.Unlock()
fake.ChangeRoleStub = nil
if fake.changeRoleReturnsOnCall == nil {
fake.changeRoleReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.changeRoleReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeMembersService) GetByFeed(arg1 context.Context, arg2 refs.FeedRef) (roomdb.Member, error) {
fake.getByFeedMutex.Lock()
ret, specificReturn := fake.getByFeedReturnsOnCall[len(fake.getByFeedArgsForCall)]
@ -637,8 +561,6 @@ func (fake *FakeMembersService) Invocations() map[string][][]interface{} {
defer fake.invocationsMutex.RUnlock()
fake.addMutex.RLock()
defer fake.addMutex.RUnlock()
fake.changeRoleMutex.RLock()
defer fake.changeRoleMutex.RUnlock()
fake.getByFeedMutex.RLock()
defer fake.getByFeedMutex.RUnlock()
fake.getByIDMutex.RLock()

View File

@ -3,9 +3,18 @@
package sqlite
import (
"context"
"database/sql"
"time"
"github.com/friendsofgo/errors"
"github.com/mattn/go-sqlite3"
"github.com/volatiletech/sqlboiler/v4/boil"
"github.com/volatiletech/sqlboiler/v4/queries/qm"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/randutil"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/sqlite/models"
)
// compiler assertion to ensure the struct fullfills the interface
@ -14,3 +23,113 @@ var _ roomdb.AuthWithSSBService = (*AuthWithSSB)(nil)
type AuthWithSSB struct {
db *sql.DB
}
const siwssbTokenLength = 32
// CreateToken is used to generate a token that is stored inside a cookie.
// It is used after a valid solution for a challenge was provided.
func (a AuthWithSSB) CreateToken(ctx context.Context, memberID int64) (string, error) {
var newToken = models.SIWSSBSession{
MemberID: memberID,
}
err := transact(a.db, func(tx *sql.Tx) error {
// check the member is registerd
if _, err := models.FindMember(ctx, tx, newToken.MemberID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return roomdb.ErrNotFound
}
return err
}
inserted := false
trying: // keep trying until we inserted in unused token
for tries := 100; tries > 0; tries-- {
// generate an new token
newToken.Token = randutil.String(siwssbTokenLength)
// insert the new token
cols := boil.Whitelist(models.SIWSSBSessionColumns.Token, models.SIWSSBSessionColumns.MemberID)
err := newToken.Insert(ctx, tx, cols)
if err != nil {
var sqlErr sqlite3.Error
if errors.As(err, &sqlErr) && sqlErr.ExtendedCode == sqlite3.ErrConstraintUnique {
// generated an existing token, retry
continue trying
}
return err
}
inserted = true
break // no error means it worked!
}
if !inserted {
return errors.New("admindb: failed to generate a fresh token in a reasonable amount of time")
}
return nil
})
if err != nil {
return "", err
}
return newToken.Token, nil
}
const sessionTimeout = time.Hour * 24
// CheckToken checks if the passed token is still valid and returns the member id if so
func (a AuthWithSSB) CheckToken(ctx context.Context, token string) (int64, error) {
var memberID int64
err := transact(a.db, func(tx *sql.Tx) error {
session, err := models.SIWSSBSessions(qm.Where("token = ?", token)).One(ctx, a.db)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return roomdb.ErrNotFound
}
return err
}
if time.Since(session.CreatedAt) > sessionTimeout {
_, err = session.Delete(ctx, tx)
if err != nil {
return err
}
return errors.New("sign-in with ssb: session expired")
}
memberID = session.MemberID
return nil
})
if err != nil {
return -1, err
}
return memberID, nil
}
// RemoveToken removes a single token from the database
func (a AuthWithSSB) RemoveToken(ctx context.Context, token string) error {
_, err := models.SIWSSBSessions(qm.Where("token = ?", token)).DeleteAll(ctx, a.db)
return err
}
// WipeTokensForMember deletes all tokens currently held for that member
func (a AuthWithSSB) WipeTokensForMember(ctx context.Context, memberID int64) error {
return transact(a.db, func(tx *sql.Tx) error {
_, err := models.SIWSSBSessions(qm.Where("member_id = ?", memberID)).DeleteAll(ctx, tx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return roomdb.ErrNotFound
}
return err
}
return nil
})
}

View File

@ -33,14 +33,14 @@ type Invites struct {
// Create creates a new invite for a new member. It returns the token or an error.
// createdBy is user ID of the admin or moderator who created it.
// aliasSuggestion is optional (empty string is fine) but can be used to disambiguate open invites. (See https://github.com/ssb-ngi-pointer/rooms2/issues/21)
// The returned token is base64 URL encoded and has tokenLength when decoded.
// The returned token is base64 URL encoded and has inviteTokenLength when decoded.
func (i Invites) Create(ctx context.Context, createdBy int64, aliasSuggestion string) (string, error) {
var newInvite = models.Invite{
CreatedBy: createdBy,
AliasSuggestion: aliasSuggestion,
}
tokenBytes := make([]byte, tokenLength)
tokenBytes := make([]byte, inviteTokenLength)
err := transact(i.db, func(tx *sql.Tx) error {
@ -88,7 +88,7 @@ func (i Invites) Create(ctx context.Context, createdBy int64, aliasSuggestion st
// Consume checks if the passed token is still valid. If it is it adds newMember to the members of the room and invalidates the token.
// If the token isn't valid, it returns an error.
// Tokens need to be base64 URL encoded and when decoded be of tokenLength.
// Tokens need to be base64 URL encoded and when decoded be of inviteTokenLength.
func (i Invites) Consume(ctx context.Context, token string, newMember refs.FeedRef) (roomdb.Invite, error) {
var inv roomdb.Invite
@ -263,7 +263,7 @@ func (i Invites) Revoke(ctx context.Context, id int64) error {
})
}
const tokenLength = 50
const inviteTokenLength = 50
func getHashedToken(b64tok string) (string, error) {
tokenBytes, err := base64.URLEncoding.DecodeString(b64tok)
@ -271,7 +271,7 @@ func getHashedToken(b64tok string) (string, error) {
return "", err
}
if n := len(tokenBytes); n != tokenLength {
if n := len(tokenBytes); n != inviteTokenLength {
return "", fmt.Errorf("admindb: invalid invite token length (only got %d bytes)", n)
}

View File

@ -18,7 +18,7 @@ CREATE TABLE fallback_passwords (
member_id INTEGER NOT NULL,
FOREIGN KEY ( member_id ) REFERENCES members( "id" )
FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE
);
CREATE INDEX fallback_passwords_by_login ON fallback_passwords(login);
@ -32,7 +32,7 @@ CREATE TABLE invites (
alias_suggestion TEXT NOT NULL DEFAULT "", -- optional
active boolean NOT NULL DEFAULT TRUE,
FOREIGN KEY ( created_by ) REFERENCES members( "id" )
FOREIGN KEY ( created_by ) REFERENCES members( "id" ) ON DELETE CASCADE
);
CREATE INDEX invite_active_ids ON invites(id) WHERE active=TRUE;
CREATE UNIQUE INDEX invite_active_tokens ON invites(hashed_token) WHERE active=TRUE;
@ -45,7 +45,7 @@ CREATE TABLE aliases (
member_id INTEGER NOT NULL,
signature BLOB NOT NULL,
FOREIGN KEY ( member_id ) REFERENCES members( "id" )
FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE
);
CREATE UNIQUE INDEX aliases_ids ON aliases(id);
CREATE UNIQUE INDEX aliases_names ON aliases(name);

View File

@ -0,0 +1,17 @@
-- +migrate Up
-- SIWSSB stands for sign-in with ssb
CREATE TABLE SIWSSB_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
token TEXT UNIQUE NOT NULL,
member_id INTEGER NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE
);
CREATE UNIQUE INDEX SIWSSB_by_token ON SIWSSB_sessions(token);
CREATE INDEX SIWSSB_by_member ON SIWSSB_sessions(member_id);
-- +migrate Down
DROP TABLE SIWSSB_sessions;
DROP INDEX SIWSSB_by_token;
DROP INDEX SIWSSB_by_member;

File diff suppressed because it is too large Load Diff

View File

@ -45,52 +45,6 @@ var AliasColumns = struct {
// Generated where
type whereHelperint64 struct{ field string }
func (w whereHelperint64) EQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) }
func (w whereHelperint64) NEQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) }
func (w whereHelperint64) LT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) }
func (w whereHelperint64) LTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) }
func (w whereHelperint64) GT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) }
func (w whereHelperint64) GTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) }
func (w whereHelperint64) IN(slice []int64) qm.QueryMod {
values := make([]interface{}, 0, len(slice))
for _, value := range slice {
values = append(values, value)
}
return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...)
}
func (w whereHelperint64) NIN(slice []int64) qm.QueryMod {
values := make([]interface{}, 0, len(slice))
for _, value := range slice {
values = append(values, value)
}
return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...)
}
type whereHelperstring struct{ field string }
func (w whereHelperstring) EQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) }
func (w whereHelperstring) NEQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) }
func (w whereHelperstring) LT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) }
func (w whereHelperstring) LTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) }
func (w whereHelperstring) GT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) }
func (w whereHelperstring) GTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) }
func (w whereHelperstring) IN(slice []string) qm.QueryMod {
values := make([]interface{}, 0, len(slice))
for _, value := range slice {
values = append(values, value)
}
return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...)
}
func (w whereHelperstring) NIN(slice []string) qm.QueryMod {
values := make([]interface{}, 0, len(slice))
for _, value := range slice {
values = append(values, value)
}
return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...)
}
type whereHelper__byte struct{ field string }
func (w whereHelper__byte) EQ(x []byte) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) }

View File

@ -4,6 +4,7 @@
package models
var TableNames = struct {
SIWSSBSessions string
Aliases string
DeniedKeys string
FallbackPasswords string
@ -13,6 +14,7 @@ var TableNames = struct {
PinNotices string
Pins string
}{
SIWSSBSessions: "SIWSSB_sessions",
Aliases: "aliases",
DeniedKeys: "denied_keys",
FallbackPasswords: "fallback_passwords",

View File

@ -67,27 +67,6 @@ func (w whereHelperroomdb_DBFeedRef) GTE(x roomdb.DBFeedRef) qm.QueryMod {
return qmhelper.Where(w.field, qmhelper.GTE, x)
}
type whereHelpertime_Time struct{ field string }
func (w whereHelpertime_Time) EQ(x time.Time) qm.QueryMod {
return qmhelper.Where(w.field, qmhelper.EQ, x)
}
func (w whereHelpertime_Time) NEQ(x time.Time) qm.QueryMod {
return qmhelper.Where(w.field, qmhelper.NEQ, x)
}
func (w whereHelpertime_Time) LT(x time.Time) qm.QueryMod {
return qmhelper.Where(w.field, qmhelper.LT, x)
}
func (w whereHelpertime_Time) LTE(x time.Time) qm.QueryMod {
return qmhelper.Where(w.field, qmhelper.LTE, x)
}
func (w whereHelpertime_Time) GT(x time.Time) qm.QueryMod {
return qmhelper.Where(w.field, qmhelper.GT, x)
}
func (w whereHelpertime_Time) GTE(x time.Time) qm.QueryMod {
return qmhelper.Where(w.field, qmhelper.GTE, x)
}
var DeniedKeyWhere = struct {
ID whereHelperint64
PubKey whereHelperroomdb_DBFeedRef

View File

@ -60,10 +60,12 @@ var MemberWhere = struct {
// MemberRels is where relationship names are stored.
var MemberRels = struct {
SIWSSBSessions string
Aliases string
FallbackPasswords string
CreatedByInvites string
}{
SIWSSBSessions: "SIWSSBSessions",
Aliases: "Aliases",
FallbackPasswords: "FallbackPasswords",
CreatedByInvites: "CreatedByInvites",
@ -71,6 +73,7 @@ var MemberRels = struct {
// memberR is where relationships are stored.
type memberR struct {
SIWSSBSessions SIWSSBSessionSlice `boil:"SIWSSBSessions" json:"SIWSSBSessions" toml:"SIWSSBSessions" yaml:"SIWSSBSessions"`
Aliases AliasSlice `boil:"Aliases" json:"Aliases" toml:"Aliases" yaml:"Aliases"`
FallbackPasswords FallbackPasswordSlice `boil:"FallbackPasswords" json:"FallbackPasswords" toml:"FallbackPasswords" yaml:"FallbackPasswords"`
CreatedByInvites InviteSlice `boil:"CreatedByInvites" json:"CreatedByInvites" toml:"CreatedByInvites" yaml:"CreatedByInvites"`
@ -366,6 +369,27 @@ func (q memberQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (boo
return count > 0, nil
}
// SIWSSBSessions retrieves all the SIWSSB_session's SIWSSBSessions with an executor.
func (o *Member) SIWSSBSessions(mods ...qm.QueryMod) sIWSSBSessionQuery {
var queryMods []qm.QueryMod
if len(mods) != 0 {
queryMods = append(queryMods, mods...)
}
queryMods = append(queryMods,
qm.Where("\"SIWSSB_sessions\".\"member_id\"=?", o.ID),
)
query := SIWSSBSessions(queryMods...)
queries.SetFrom(query.Query, "\"SIWSSB_sessions\"")
if len(queries.GetSelect(query.Query)) == 0 {
queries.SetSelect(query.Query, []string{"\"SIWSSB_sessions\".*"})
}
return query
}
// Aliases retrieves all the alias's Aliases with an executor.
func (o *Member) Aliases(mods ...qm.QueryMod) aliasQuery {
var queryMods []qm.QueryMod
@ -429,6 +453,104 @@ func (o *Member) CreatedByInvites(mods ...qm.QueryMod) inviteQuery {
return query
}
// LoadSIWSSBSessions allows an eager lookup of values, cached into the
// loaded structs of the objects. This is for a 1-M or N-M relationship.
func (memberL) LoadSIWSSBSessions(ctx context.Context, e boil.ContextExecutor, singular bool, maybeMember interface{}, mods queries.Applicator) error {
var slice []*Member
var object *Member
if singular {
object = maybeMember.(*Member)
} else {
slice = *maybeMember.(*[]*Member)
}
args := make([]interface{}, 0, 1)
if singular {
if object.R == nil {
object.R = &memberR{}
}
args = append(args, object.ID)
} else {
Outer:
for _, obj := range slice {
if obj.R == nil {
obj.R = &memberR{}
}
for _, a := range args {
if a == obj.ID {
continue Outer
}
}
args = append(args, obj.ID)
}
}
if len(args) == 0 {
return nil
}
query := NewQuery(
qm.From(`SIWSSB_sessions`),
qm.WhereIn(`SIWSSB_sessions.member_id in ?`, args...),
)
if mods != nil {
mods.Apply(query)
}
results, err := query.QueryContext(ctx, e)
if err != nil {
return errors.Wrap(err, "failed to eager load SIWSSB_sessions")
}
var resultSlice []*SIWSSBSession
if err = queries.Bind(results, &resultSlice); err != nil {
return errors.Wrap(err, "failed to bind eager loaded slice SIWSSB_sessions")
}
if err = results.Close(); err != nil {
return errors.Wrap(err, "failed to close results in eager load on SIWSSB_sessions")
}
if err = results.Err(); err != nil {
return errors.Wrap(err, "error occurred during iteration of eager loaded relations for SIWSSB_sessions")
}
if len(sIWSSBSessionAfterSelectHooks) != 0 {
for _, obj := range resultSlice {
if err := obj.doAfterSelectHooks(ctx, e); err != nil {
return err
}
}
}
if singular {
object.R.SIWSSBSessions = resultSlice
for _, foreign := range resultSlice {
if foreign.R == nil {
foreign.R = &sIWSSBSessionR{}
}
foreign.R.Member = object
}
return nil
}
for _, foreign := range resultSlice {
for _, local := range slice {
if local.ID == foreign.MemberID {
local.R.SIWSSBSessions = append(local.R.SIWSSBSessions, foreign)
if foreign.R == nil {
foreign.R = &sIWSSBSessionR{}
}
foreign.R.Member = local
break
}
}
}
return nil
}
// LoadAliases allows an eager lookup of values, cached into the
// loaded structs of the objects. This is for a 1-M or N-M relationship.
func (memberL) LoadAliases(ctx context.Context, e boil.ContextExecutor, singular bool, maybeMember interface{}, mods queries.Applicator) error {
@ -723,6 +845,59 @@ func (memberL) LoadCreatedByInvites(ctx context.Context, e boil.ContextExecutor,
return nil
}
// AddSIWSSBSessions adds the given related objects to the existing relationships
// of the member, optionally inserting them as new records.
// Appends related to o.R.SIWSSBSessions.
// Sets related.R.Member appropriately.
func (o *Member) AddSIWSSBSessions(ctx context.Context, exec boil.ContextExecutor, insert bool, related ...*SIWSSBSession) error {
var err error
for _, rel := range related {
if insert {
rel.MemberID = o.ID
if err = rel.Insert(ctx, exec, boil.Infer()); err != nil {
return errors.Wrap(err, "failed to insert into foreign table")
}
} else {
updateQuery := fmt.Sprintf(
"UPDATE \"SIWSSB_sessions\" SET %s WHERE %s",
strmangle.SetParamNames("\"", "\"", 0, []string{"member_id"}),
strmangle.WhereClause("\"", "\"", 0, sIWSSBSessionPrimaryKeyColumns),
)
values := []interface{}{o.ID, rel.ID}
if boil.IsDebug(ctx) {
writer := boil.DebugWriterFrom(ctx)
fmt.Fprintln(writer, updateQuery)
fmt.Fprintln(writer, values)
}
if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil {
return errors.Wrap(err, "failed to update foreign table")
}
rel.MemberID = o.ID
}
}
if o.R == nil {
o.R = &memberR{
SIWSSBSessions: related,
}
} else {
o.R.SIWSSBSessions = append(o.R.SIWSSBSessions, related...)
}
for _, rel := range related {
if rel.R == nil {
rel.R = &sIWSSBSessionR{
Member: o,
}
} else {
rel.R.Member = o
}
}
return nil
}
// AddAliases adds the given related objects to the existing relationships
// of the member, optionally inserting them as new records.
// Appends related to o.R.Aliases.

View File

@ -27,13 +27,13 @@ import (
migrate "github.com/rubenv/sql-migrate"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
)
type Database struct {
db *sql.DB
AuthFallback roomdb.AuthFallbackService
AuthFallback AuthFallback
AuthWithSSB AuthWithSSB
Members Members
Aliases Aliases
@ -102,6 +102,7 @@ func Open(r repo.Interface) (*Database, error) {
Aliases: Aliases{db},
AuthFallback: AuthFallback{db},
AuthWithSSB: AuthWithSSB{db},
DeniedKeys: DeniedKeys{db},
Invites: Invites{db: db, members: ml},
Notices: Notices{db},

View File

@ -8,9 +8,10 @@ import (
"testing"
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/require"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/stretchr/testify/require"
refs "go.mindeco.de/ssb-refs"
)

View File

@ -4,17 +4,18 @@ package roomsrv
import (
kitlog "github.com/go-kit/kit/log"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
muxrpc "go.cryptoscope.co/muxrpc/v2"
"go.cryptoscope.co/muxrpc/v2/typemux"
"github.com/ssb-ngi-pointer/go-ssb-room/muxrpc/handlers/alias"
"github.com/ssb-ngi-pointer/go-ssb-room/muxrpc/handlers/gossip"
"github.com/ssb-ngi-pointer/go-ssb-room/muxrpc/handlers/signinwithssb"
"github.com/ssb-ngi-pointer/go-ssb-room/muxrpc/handlers/tunnel/server"
"github.com/ssb-ngi-pointer/go-ssb-room/muxrpc/handlers/whoami"
)
// instantiate and register the muxrpc handlers
func (s *Server) initHandlers(aliasDB roomdb.AliasesService) {
func (s *Server) initHandlers() {
// inistaniate handler packages
whoami := whoami.New(s.Whoami())
@ -27,10 +28,19 @@ func (s *Server) initHandlers(aliasDB roomdb.AliasesService) {
aliasHandler := alias.New(
kitlog.With(s.logger, "unit", "aliases"),
s.Whoami(),
aliasDB,
s.Aliases,
s.domain,
)
siwssbHandler := signinwithssb.New(
kitlog.With(s.logger, "unit", "auth-with-ssb"),
s.Whoami(),
s.domain,
s.Members,
s.authWithSSB,
s.authWithSSBBridge,
)
// register muxrpc commands
registries := []typemux.HandlerMux{s.public, s.master}
@ -47,5 +57,12 @@ func (s *Server) initHandlers(aliasDB roomdb.AliasesService) {
mux.RegisterAsync(append(method, "registerAlias"), typemux.AsyncFunc(aliasHandler.Register))
mux.RegisterAsync(append(method, "revokeAlias"), typemux.AsyncFunc(aliasHandler.Revoke))
method = muxrpc.Method{"httpAuth"}
mux.RegisterAsync(append(method, "invalidateAllSolutions"), typemux.AsyncFunc(siwssbHandler.InvalidateAllSolutions))
mux.RegisterAsync(append(method, "sendSolution"), typemux.AsyncFunc(siwssbHandler.SendSolution))
method = muxrpc.Method{"gossip"}
mux.RegisterDuplex(append(method, "ping"), typemux.DuplexFunc(gossip.Ping))
}
}

View File

@ -23,6 +23,7 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/multicloser"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
refs "go.mindeco.de/ssb-refs"
@ -66,6 +67,9 @@ type Server struct {
Members roomdb.MembersService
Aliases roomdb.AliasesService
authWithSSB roomdb.AuthWithSSBService
authWithSSBBridge *signinwithssb.SignalBridge
}
func (s Server) Whoami() refs.FeedRef {
@ -75,6 +79,8 @@ func (s Server) Whoami() refs.FeedRef {
func New(
membersdb roomdb.MembersService,
aliasdb roomdb.AliasesService,
awsdb roomdb.AuthWithSSBService,
bridge *signinwithssb.SignalBridge,
domainName string,
opts ...Option,
) (*Server, error) {
@ -84,6 +90,9 @@ func New(
s.Members = membersdb
s.Aliases = aliasdb
s.authWithSSB = awsdb
s.authWithSSBBridge = bridge
s.domain = domainName
for i, opt := range opts {
@ -143,7 +152,7 @@ func New(
s.StateManager = roomstate.NewManager(s.rootCtx, s.logger)
s.initHandlers(aliasdb)
s.initHandlers()
if err := s.initNetwork(); err != nil {
return nil, err

View File

@ -0,0 +1,30 @@
const ssbUriLink = document.querySelector('#start-auth-uri');
const waitingElem = document.querySelector('#waiting');
const errorElem = document.querySelector('#failed');
const challengeElem = document.querySelector('#challenge');
const sc = challengeElem.dataset.sc;
const evtSource = new EventSource(`/withssb/events?sc=${sc}`);
ssbUriLink.addEventListener('click', (e) => {
errorElem.classList.add('hidden');
waitingElem.classList.remove('hidden');
});
evtSource.onerror = (e) => {
waitingElem.classList.add('hidden');
errorElem.classList.remove('hidden');
console.error(e.data);
};
evtSource.addEventListener('failed', (e) => {
waitingElem.classList.add('hidden');
errorElem.classList.remove('hidden');
console.error(e.data);
});
evtSource.addEventListener('success', (e) => {
waitingElem.classList.add('hidden');
evtSource.close();
window.location = `/withssb/finalize?token=${e.data}`;
});

View File

@ -4,6 +4,7 @@
package errors
import (
"errors"
"fmt"
)
@ -23,3 +24,13 @@ type ErrBadRequest struct {
func (br ErrBadRequest) Error() string {
return fmt.Sprintf("rooms/web: bad request error: %s", br.Details)
}
type ErrForbidden struct {
Details error
}
func (f ErrForbidden) Error() string {
return fmt.Sprintf("rooms/web: access denied: %s", f.Details)
}
var ErrNotAuthorized = errors.New("rooms/web: not authorized")

View File

@ -4,7 +4,6 @@ package admin
import (
"context"
"math/rand"
"net/http"
"testing"
"time"
@ -16,6 +15,7 @@ import (
"go.mindeco.de/http/tester"
"go.mindeco.de/logging/logtest"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/randutil"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
@ -60,7 +60,7 @@ func newSession(t *testing.T) *testSession {
ts.Router = router.CompleteApp()
ts.Domain = randomString(10)
ts.Domain = randutil.String(10)
// fake user
ts.User = roomdb.Member{
@ -120,15 +120,3 @@ func newSession(t *testing.T) *testSession {
return &ts
}
// utils
func randomString(n int) string {
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
s := make([]rune, n)
for i := range s {
s[i] = letters[rand.Intn(len(letters))]
}
return string(s)
}

View File

@ -4,8 +4,10 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"log"
"net/http"
"net/url"
"github.com/gorilla/mux"
"go.mindeco.de/http/render"
@ -46,7 +48,7 @@ func (a aliasHandler) resolve(rw http.ResponseWriter, req *http.Request) {
alias, err := a.db.Resolve(req.Context(), name)
if err != nil {
ar.SendError(err)
ar.SendError(fmt.Errorf("aliases: failed to resolve name %q: %w", name, err))
return
}
@ -139,11 +141,29 @@ func (html *aliasHTMLResponder) UpdateRoomInfo(hostAndPort string, roomID refs.F
}
func (html aliasHTMLResponder) SendConfirmation(alias roomdb.Alias) {
// construct the ssb:experimental?action=consume-alias&... uri for linking into apps
queryParams := url.Values{}
queryParams.Set("action", "consume-alias")
queryParams.Set("roomId", html.roomID.Ref())
queryParams.Set("alias", alias.Name)
queryParams.Set("userId", alias.Feed.Ref())
queryParams.Set("signature", base64.URLEncoding.EncodeToString(alias.Signature))
queryParams.Set("multiserverAddress", html.multiservAddr)
// html.multiservAddr
ssbURI := url.URL{
Scheme: "ssb",
Opaque: "experimental",
RawQuery: queryParams.Encode(),
}
err := html.renderer.Render(html.rw, html.req, "aliases-resolved.html", http.StatusOK, struct {
Alias roomdb.Alias
RoomAddr string
}{alias, html.multiservAddr})
SSBURI template.URL
}{alias, template.URL(ssbURI.String())})
if err != nil {
log.Println("alias-resolve render errr:", err)
}

View File

@ -1,36 +0,0 @@
// SPDX-License-Identifier: MIT
package auth
import (
"net/http"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
"go.mindeco.de/http/auth"
"go.mindeco.de/http/render"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
)
var HTMLTemplates = []string{
"auth/fallback_sign_in.tmpl",
}
func Handler(m *mux.Router, r *render.Renderer, a *auth.Handler) http.Handler {
if m == nil {
m = router.Auth(nil)
}
m.Get(router.AuthFallbackSignInForm).Handler(r.HTML("auth/fallback_sign_in.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
return map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(req),
}, nil
}))
// hook up the auth handler to the router
m.Get(router.AuthFallbackSignIn).HandlerFunc(a.Authorize)
m.Get(router.AuthFallbackSignOut).HandlerFunc(a.Logout)
return m
}

View File

@ -0,0 +1,514 @@
// SPDX-License-Identifier: MIT
package auth
import (
"context"
"encoding/base64"
"encoding/gob"
"fmt"
"html/template"
"image/color"
"io"
"net/http"
"net/url"
"strings"
"time"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/skip2/go-qrcode"
"go.cryptoscope.co/muxrpc/v2"
"go.mindeco.de/http/render"
"go.mindeco.de/logging"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
refs "go.mindeco.de/ssb-refs"
)
var HTMLTemplates = []string{
"auth/decide_method.tmpl",
"auth/withssb_server_start.tmpl",
}
// custom sessionKey type to prevent collision
type sessionKey uint
func init() {
// need to register our Key with gob so gorilla/sessions can (de)serialize it
gob.Register(memberToken)
gob.Register(time.Time{})
}
const (
siwssbSessionName = "AuthWithSSBSession"
memberToken sessionKey = iota
userTimeout
)
const sessionLifetime = time.Hour * 24
// WithSSBHandler implements the oauth-like challenge/response dance described in
// https://ssb-ngi-pointer.github.io/ssb-http-auth-spec
type WithSSBHandler struct {
render *render.Renderer
roomID refs.FeedRef
membersdb roomdb.MembersService
aliasesdb roomdb.AliasesService
sessiondb roomdb.AuthWithSSBService
cookieStore sessions.Store
endpoints network.Endpoints
bridge *signinwithssb.SignalBridge
}
func NewWithSSBHandler(
m *mux.Router,
r *render.Renderer,
roomID refs.FeedRef,
endpoints network.Endpoints,
aliasDB roomdb.AliasesService,
membersDB roomdb.MembersService,
sessiondb roomdb.AuthWithSSBService,
cookies sessions.Store,
bridge *signinwithssb.SignalBridge,
) *WithSSBHandler {
var ssb WithSSBHandler
ssb.render = r
ssb.roomID = roomID
ssb.aliasesdb = aliasDB
ssb.membersdb = membersDB
ssb.endpoints = endpoints
ssb.sessiondb = sessiondb
ssb.cookieStore = cookies
ssb.bridge = bridge
m.Get(router.AuthWithSSBLogin).HandlerFunc(ssb.decideMethod)
m.Get(router.AuthWithSSBServerEvents).HandlerFunc(ssb.eventSource)
m.Get(router.AuthWithSSBFinalize).HandlerFunc(ssb.finalizeCookie)
return &ssb
}
// AuthenticateRequest uses the passed request to load and return the session data that was stored previously.
// If it is invalid or there is no session, it will return ErrNotAuthorized.
// Otherwise it will return the member that belongs to the session.
func (h WithSSBHandler) AuthenticateRequest(r *http.Request) (*roomdb.Member, error) {
session, err := h.cookieStore.Get(r, siwssbSessionName)
if err != nil {
return nil, err
}
if session.IsNew {
return nil, weberrors.ErrNotAuthorized
}
tokenVal, ok := session.Values[memberToken]
if !ok {
return nil, weberrors.ErrNotAuthorized
}
t, ok := session.Values[userTimeout]
if !ok {
return nil, weberrors.ErrNotAuthorized
}
tout, ok := t.(time.Time)
if !ok {
return nil, weberrors.ErrNotAuthorized
}
if time.Now().After(tout) {
return nil, weberrors.ErrNotAuthorized
}
token, ok := tokenVal.(string)
if !ok {
return nil, weberrors.ErrNotAuthorized
}
memberID, err := h.sessiondb.CheckToken(r.Context(), token)
if err != nil {
return nil, err
}
member, err := h.membersdb.GetByID(r.Context(), memberID)
if err != nil {
return nil, err
}
return &member, nil
}
// Logout destroys the session data and updates the cookie with an invalidated one.
func (h WithSSBHandler) Logout(w http.ResponseWriter, r *http.Request) error {
session, err := h.cookieStore.Get(r, siwssbSessionName)
if err != nil {
return err
}
tokenVal, ok := session.Values[memberToken]
if !ok {
// not a ssb http auth session
return nil
}
token, ok := tokenVal.(string)
if !ok {
return fmt.Errorf("wrong token type: %T", tokenVal)
}
err = h.sessiondb.RemoveToken(r.Context(), token)
if err != nil {
return err
}
session.Values[userTimeout] = time.Now().Add(-sessionLifetime)
session.Options.MaxAge = -1
if err := session.Save(r, w); err != nil {
return err
}
return nil
}
// saveCookie is a utility function that stores the passed token inside the cookie
func (h WithSSBHandler) saveCookie(w http.ResponseWriter, req *http.Request, token string) error {
session, err := h.cookieStore.Get(req, siwssbSessionName)
if err != nil {
err = fmt.Errorf("ssb http auth: failed to load cookie session: %w", err)
return err
}
session.Values[memberToken] = token
session.Values[userTimeout] = time.Now().Add(sessionLifetime)
if err := session.Save(req, w); err != nil {
err = fmt.Errorf("ssb http auth: failed to update cookie session: %w", err)
return err
}
return nil
}
// this is the /login landing page which branches out to the different methods based on the query parameters that are present
func (h WithSSBHandler) decideMethod(w http.ResponseWriter, req *http.Request) {
queryVals := req.URL.Query()
var (
alias string = queryVals.Get("alias")
cid *refs.FeedRef
)
if cidString := queryVals.Get("cid"); cidString != "" {
parsedCID, err := refs.ParseFeedRef(cidString)
if err == nil {
cid = parsedCID
_, err := h.membersdb.GetByFeed(req.Context(), *cid)
if err != nil {
if err == roomdb.ErrNotFound {
errMsg := fmt.Errorf("ssb http auth: client isn't a member: %w", err)
h.render.Error(w, req, http.StatusForbidden, errMsg)
return
}
h.render.Error(w, req, http.StatusInternalServerError, err)
return
}
}
} else {
aliasEntry, err := h.aliasesdb.Resolve(req.Context(), alias)
if err == nil {
cid = &aliasEntry.Feed
}
}
// ?cid=CID&cc=CC does client-initiated http-auth
if cc := queryVals.Get("cc"); cc != "" && cid != nil {
err := h.clientInitiated(w, req, *cid)
if err != nil {
h.render.Error(w, req, http.StatusInternalServerError, err)
}
return
}
// assume server-init sse dance
data, err := h.serverInitiated()
if err != nil {
h.render.Error(w, req, http.StatusInternalServerError, err)
return
}
h.render.Render(w, req, "auth/withssb_server_start.tmpl", http.StatusOK, data)
}
// clientInitiated is called with a client challange (?cc=123) and calls back to the passed client using muxrpc to request a signed solution
// if everything checks out it redirects to the admin dashboard
func (h WithSSBHandler) clientInitiated(w http.ResponseWriter, req *http.Request, client refs.FeedRef) error {
queryParams := req.URL.Query()
var payload signinwithssb.ClientPayload
payload.ServerID = h.roomID // fill in the server
// validate and update client challenge
cc := queryParams.Get("cc")
payload.ClientChallenge = cc
// check that we have that member
member, err := h.membersdb.GetByFeed(req.Context(), client)
if err != nil {
errMsg := fmt.Errorf("ssb http auth: client isn't a member: %w", err)
if err == roomdb.ErrNotFound {
return weberrors.ErrForbidden{Details: errMsg}
}
return errMsg
}
payload.ClientID = client
// get the connected client for that member
edp, connected := h.endpoints.GetEndpointFor(client)
if !connected {
return weberrors.ErrForbidden{Details: fmt.Errorf("ssb http auth: client not connected to room")}
}
// roll a Challenge from the server
sc := signinwithssb.GenerateChallenge()
payload.ServerChallenge = sc
ctx, cancel := context.WithTimeout(req.Context(), 1*time.Minute)
defer cancel()
// request the signed solution over muxrpc
var solution string
err = edp.Async(ctx, &solution, muxrpc.TypeString, muxrpc.Method{"httpAuth", "requestSolution"}, sc, cc)
if err != nil {
return fmt.Errorf("ssb http auth: could not request solution from client: %w", err)
}
// decode and validate the response
solution = strings.TrimSuffix(solution, ".sig.ed25519")
solutionBytes, err := base64.StdEncoding.DecodeString(solution)
if err != nil {
return fmt.Errorf("ssb http auth: failed to decode solution: %w", err)
}
if !payload.Validate(solutionBytes) {
return fmt.Errorf("ssb http auth: validation of client solution failed")
}
// create a session for invalidation
tok, err := h.sessiondb.CreateToken(req.Context(), member.ID)
if err != nil {
err = fmt.Errorf("ssb http auth: could not create token: %w", err)
return err
}
if err := h.saveCookie(w, req, tok); err != nil {
return err
}
// go to the dashboard
dashboardURL, err := router.CompleteApp().Get(router.AdminDashboard).URL()
if err != nil {
return err
}
http.Redirect(w, req, dashboardURL.Path, http.StatusTemporaryRedirect)
return nil
}
// server-sent-events stuff
type templateData struct {
SSBURI template.URL
QRCodeURI template.URL
ServerChallenge string
}
func (h WithSSBHandler) serverInitiated() (templateData, error) {
sc := h.bridge.RegisterSession()
// prepare the ssb-uri
// https://ssb-ngi-pointer.github.io/ssb-http-auth-spec/#list-of-new-ssb-uris
var queryParams = make(url.Values)
queryParams.Set("action", "start-http-auth")
queryParams.Set("sid", h.roomID.Ref())
queryParams.Set("sc", sc)
var startAuthURI url.URL
startAuthURI.Scheme = "ssb"
startAuthURI.Opaque = "experimental"
startAuthURI.RawQuery = queryParams.Encode()
// generate a QR code with the token inside so that you can open it easily in a supporting mobile app
qrCode, err := qrcode.New(startAuthURI.String(), qrcode.Medium)
if err != nil {
return templateData{}, err
}
qrCode.BackgroundColor = color.Transparent // transparent to fit into the page
qrCode.ForegroundColor = color.Black
qrCodeData, err := qrCode.PNG(-5)
if err != nil {
return templateData{}, err
}
qrURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(qrCodeData)
// template.URL signals the template engine that those aren't fishy and from a trusted source
data := templateData{
SSBURI: template.URL(startAuthURI.String()),
QRCodeURI: template.URL(qrURI),
ServerChallenge: sc,
}
return data, nil
}
// finalizeCookie is called with a redirect from the js sse client if everything worked
func (h WithSSBHandler) finalizeCookie(w http.ResponseWriter, r *http.Request) {
tok := r.URL.Query().Get("token")
// check the token is correct
if _, err := h.sessiondb.CheckToken(r.Context(), tok); err != nil {
http.Error(w, "invalid session token", http.StatusForbidden)
return
}
if err := h.saveCookie(w, r, tok); err != nil {
http.Error(w, "failed to save cookie", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
// the time after which the SSE dance is considered failed
const sseTimeout = 3 * time.Minute
// eventSource is the server-side of our server-sent events (SSE) session
// https://html.spec.whatwg.org/multipage/server-sent-events.html
func (h WithSSBHandler) eventSource(w http.ResponseWriter, r *http.Request) {
flusher, err := w.(http.Flusher)
if !err {
http.Error(w, "ssb http auth: server-initiated method needs streaming support", http.StatusInternalServerError)
return
}
// closes when the http request is closed
var notify <-chan bool
notifier, ok := w.(http.CloseNotifier)
if !ok {
// testing hack
// http.Error(w, "ssb http auth: cant notify about closed requests", http.StatusInternalServerError)
// return
ch := make(chan bool)
go func() {
time.Sleep(sseTimeout)
close(ch)
}()
notify = ch
} else {
notify = notifier.CloseNotify()
}
// setup headers for SSE
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Transfer-Encoding", "chunked")
sc := r.URL.Query().Get("sc")
if sc == "" {
http.Error(w, "missing server challenge", http.StatusBadRequest)
return
}
logger := logging.FromContext(r.Context())
logger = level.Debug(logger)
logger = kitlog.With(logger, "stream", sc[:5])
logger.Log("event", "stream opened")
evtCh, has := h.bridge.GetEventChannel(sc)
if !has {
http.Error(w, "no such session!", http.StatusBadRequest)
return
}
sender := newEventSender(w)
// ping ticker
tick := time.NewTicker(3 * time.Second)
go func() {
time.Sleep(sseTimeout)
tick.Stop()
logger.Log("event", "stopped")
}()
start := time.Now()
flusher.Flush()
// Push events to client
for {
select {
case <-notify:
logger.Log("event", "request closed")
return
case <-tick.C:
sender.send("ping", fmt.Sprintf("Waiting for solution (session age: %s)", time.Since(start)))
logger.Log("event", "sent ping")
case update := <-evtCh:
var event, data string = "failed", "challenge validation failed"
if update.Worked {
event = "success"
data = update.Token
} else {
if update.Reason != nil {
data = update.Reason.Error()
}
}
sender.send(event, data)
logger.Log("event", "sent", "worked", update.Worked)
return
}
flusher.Flush()
}
}
// eventSender encapsulates the event ID and increases it with each send automatically
type eventSender struct {
w io.Writer
id uint32
}
func newEventSender(w io.Writer) eventSender {
return eventSender{w: w}
}
func (es *eventSender) send(event, data string) {
fmt.Fprintf(es.w, "id: %d\n", es.id)
fmt.Fprintf(es.w, "data: %s\n", data)
fmt.Fprintf(es.w, "event: %s\n", event)
fmt.Fprint(es.w, "\n")
es.id++
}

View File

@ -1,15 +1,27 @@
// SPDX-License-Identifier: MIT
package handlers
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.cryptoscope.co/muxrpc/v2"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/keys"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
"github.com/ssb-ngi-pointer/go-ssb-room/web/webassert"
refs "go.mindeco.de/ssb-refs"
@ -38,14 +50,14 @@ func TestLoginForm(t *testing.T) {
a, r := assert.New(t), require.New(t)
url, err := ts.Router.Get(router.AuthFallbackSignInForm).URL()
url, err := ts.Router.Get(router.AuthFallbackLogin).URL()
r.Nil(err)
html, resp := ts.Client.GetHTML(url.String())
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
{"title", "AuthTitle"},
{"#welcome", "AuthFallbackWelcome"},
{"title", "AuthFallbackTitle"},
})
}
@ -53,11 +65,11 @@ func TestFallbackAuth(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
// very cheap client session
// very cheap "browser" client session
jar, err := cookiejar.New(nil)
r.NoError(err)
signInFormURL, err := ts.Router.Get(router.AuthFallbackSignInForm).URL()
signInFormURL, err := ts.Router.Get(router.AuthFallbackLogin).URL()
r.Nil(err)
signInFormURL.Host = "localhost"
signInFormURL.Scheme = "https"
@ -70,9 +82,10 @@ func TestFallbackAuth(t *testing.T) {
jar.SetCookies(signInFormURL, csrfCookie)
webassert.CSRFTokenPresent(t, doc.Find("form"))
passwordForm := doc.Find("#password-fallback")
webassert.CSRFTokenPresent(t, passwordForm)
csrfTokenElem := doc.Find("input[type=hidden]")
csrfTokenElem := passwordForm.Find("input[type=hidden]")
a.Equal(1, csrfTokenElem.Length())
csrfName, has := csrfTokenElem.Attr("name")
@ -89,7 +102,7 @@ func TestFallbackAuth(t *testing.T) {
}
ts.AuthFallbackDB.CheckReturns(int64(23), nil)
signInURL, err := ts.Router.Get(router.AuthFallbackSignIn).URL()
signInURL, err := ts.Router.Get(router.AuthFallbackFinalize).URL()
r.Nil(err)
signInURL.Host = "localhost"
@ -112,6 +125,7 @@ func TestFallbackAuth(t *testing.T) {
sessionCookie := resp.Result().Cookies()
jar.SetCookies(signInURL, sessionCookie)
// now request the protected dashboard page
dashboardURL, err := ts.Router.Get(router.AdminDashboard).URL()
r.Nil(err)
dashboardURL.Host = "localhost"
@ -169,3 +183,382 @@ func TestFallbackAuth(t *testing.T) {
{"#roomCount", "AdminRoomCountPlural"},
})
}
func TestAuthWithSSBClientInitNotConnected(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
// the client is a member but not connected right now
ts.MembersDB.GetByFeedReturns(roomdb.Member{ID: 1234, Nickname: "test-member"}, nil)
ts.MockedEndpoints.GetEndpointForReturns(nil, false)
client, err := keys.NewKeyPair(nil)
r.NoError(err)
cc := signinwithssb.GenerateChallenge()
urlTo := web.NewURLTo(ts.Router)
signInStartURL := urlTo(router.AuthWithSSBLogin,
"cid", client.Feed.Ref(),
"cc", cc,
)
r.NotNil(signInStartURL)
t.Log(signInStartURL.String())
doc, resp := ts.Client.GetHTML(signInStartURL.String())
a.Equal(http.StatusInternalServerError, resp.Code) // TODO: StatusForbidden
webassert.Localized(t, doc, []webassert.LocalizedElement{
// {"#welcome", "AuthWithSSBWelcome"},
// {"title", "AuthWithSSBTitle"},
})
}
func TestAuthWithSSBClientInitNotAllowed(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
// the client isnt a member
ts.MembersDB.GetByFeedReturns(roomdb.Member{}, roomdb.ErrNotFound)
ts.MockedEndpoints.GetEndpointForReturns(nil, false)
client, err := keys.NewKeyPair(nil)
r.NoError(err)
cc := signinwithssb.GenerateChallenge()
urlTo := web.NewURLTo(ts.Router)
signInStartURL := urlTo(router.AuthWithSSBLogin,
"cid", client.Feed.Ref(),
"cc", cc,
)
r.NotNil(signInStartURL)
t.Log(signInStartURL.String())
doc, resp := ts.Client.GetHTML(signInStartURL.String())
a.Equal(http.StatusForbidden, resp.Code)
webassert.Localized(t, doc, []webassert.LocalizedElement{
// {"#welcome", "AuthWithSSBWelcome"},
// {"title", "AuthWithSSBTitle"},
})
}
func TestAuthWithSSBClientInitHasClient(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
// very cheap "browser" client session
jar, err := cookiejar.New(nil)
r.NoError(err)
// the request to be signed later
var payload signinwithssb.ClientPayload
payload.ServerID = ts.NetworkInfo.RoomID
// the keypair for our client
testMember := roomdb.Member{ID: 1234, Nickname: "test-member"}
client, err := keys.NewKeyPair(nil)
r.NoError(err)
testMember.PubKey = client.Feed
// setup the mocked database
ts.MembersDB.GetByFeedReturns(testMember, nil)
ts.AuthWithSSB.CreateTokenReturns("abcdefgh", nil)
ts.AuthWithSSB.CheckTokenReturns(testMember.ID, nil)
ts.MembersDB.GetByIDReturns(testMember, nil)
// fill the basic infos of the request
payload.ClientID = client.Feed
// this is our fake "connected" client
var edp muxrpc.FakeEndpoint
// setup a mocked muxrpc call that asserts the arguments and returns the needed signature
edp.AsyncCalls(func(_ context.Context, ret interface{}, encoding muxrpc.RequestEncoding, method muxrpc.Method, args ...interface{}) error {
a.Equal(muxrpc.TypeString, encoding)
a.Equal("httpAuth.requestSolution", method.String())
r.Len(args, 2, "expected two args")
serverChallenge, ok := args[0].(string)
r.True(ok, "argument[0] is not a string: %T", args[0])
a.NotEqual("", serverChallenge)
// update the challenge
payload.ServerChallenge = serverChallenge
clientChallenge, ok := args[1].(string)
r.True(ok, "argument[1] is not a string: %T", args[1])
a.Equal(payload.ClientChallenge, clientChallenge)
strptr, ok := ret.(*string)
r.True(ok, "return is not a string pointer: %T", ret)
// sign the request now that we have the sc
clientSig := payload.Sign(client.Pair.Secret)
*strptr = base64.StdEncoding.EncodeToString(clientSig)
return nil
})
// setup the fake client endpoint
ts.MockedEndpoints.GetEndpointForReturns(&edp, true)
cc := signinwithssb.GenerateChallenge()
// update the challenge
payload.ClientChallenge = cc
// prepare the url
urlTo := web.NewURLTo(ts.Router)
signInStartURL := urlTo(router.AuthWithSSBLogin,
"cid", client.Feed.Ref(),
"cc", cc,
)
signInStartURL.Host = "localhost"
signInStartURL.Scheme = "https"
r.NotNil(signInStartURL)
t.Log(signInStartURL.String())
doc, resp := ts.Client.GetHTML(signInStartURL.String())
a.Equal(http.StatusTemporaryRedirect, resp.Code)
dashboardURL, err := ts.Router.Get(router.AdminDashboard).URL()
r.Nil(err)
a.Equal(dashboardURL.Path, resp.Header().Get("Location"))
webassert.Localized(t, doc, []webassert.LocalizedElement{
// {"#welcome", "AuthWithSSBWelcome"},
// {"title", "AuthWithSSBTitle"},
})
// analyse the endpoints call
a.Equal(1, ts.MockedEndpoints.GetEndpointForCallCount())
edpRef := ts.MockedEndpoints.GetEndpointForArgsForCall(0)
a.Equal(client.Feed.Ref(), edpRef.Ref())
// check the mock was called
a.Equal(1, edp.AsyncCallCount())
// check that we have a new cookie
sessionCookie := resp.Result().Cookies()
r.True(len(sessionCookie) > 0, "expecting one cookie!")
jar.SetCookies(signInStartURL, sessionCookie)
// now request the protected dashboard page
dashboardURL.Host = "localhost"
dashboardURL.Scheme = "https"
// load the cookie for the dashboard
cs := jar.Cookies(dashboardURL)
r.True(len(cs) > 0, "expecting one cookie!")
var sessionHeader = http.Header(map[string][]string{})
for _, c := range cs {
theCookie := c.String()
a.NotEqual("", theCookie, "should have a new cookie")
sessionHeader.Add("Cookie", theCookie)
}
durl := dashboardURL.String()
t.Log(durl)
// update headers
ts.Client.ClearHeaders()
ts.Client.SetHeaders(sessionHeader)
html, resp := ts.Client.GetHTML(durl)
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
t.Log(html.Find("body").Text())
}
webassert.Localized(t, html, []webassert.LocalizedElement{
{"#welcome", "AdminDashboardWelcome"},
{"title", "AdminDashboardTitle"},
})
}
func TestAuthWithSSBServerInitHappyPath(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
// the keypair for our client
testMember := roomdb.Member{ID: 1234, Nickname: "test-member"}
client, err := keys.NewKeyPair(nil)
r.NoError(err)
testMember.PubKey = client.Feed
// setup the mocked database
ts.MembersDB.GetByFeedReturns(testMember, nil)
// prepare the url
urlTo := web.NewURLTo(ts.Router)
signInStartURL := urlTo(router.AuthWithSSBLogin,
"cid", client.Feed.Ref(),
)
r.NotNil(signInStartURL)
html, resp := ts.Client.GetHTML(signInStartURL.String())
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
t.Log(html.Find("body").Text())
}
webassert.Localized(t, html, []webassert.LocalizedElement{
{"title", "AuthWithSSBTitle"},
{"#welcome", "AuthWithSSBWelcome"},
})
jsFile, has := html.Find("script").Attr("src")
a.True(has, "should have client code")
a.Equal("/assets/login-events.js", jsFile)
serverChallenge, has := html.Find("#challenge").Attr("data-sc")
a.True(has, "should have server challenge")
a.NotEqual("", serverChallenge)
ssbURI, has := html.Find("#start-auth-uri").Attr("href")
a.True(has, "should have an ssb:experimental uri")
a.True(strings.HasPrefix(ssbURI, "ssb:experimental?"), "not an ssb-uri? %s", ssbURI)
parsedURI, err := url.Parse(ssbURI)
r.NoError(err)
a.Equal("ssb", parsedURI.Scheme)
a.Equal("experimental", parsedURI.Opaque)
qry := parsedURI.Query()
a.Equal("start-http-auth", qry.Get("action"))
a.Equal(serverChallenge, qry.Get("sc"))
a.Equal(ts.NetworkInfo.RoomID.Ref(), qry.Get("sid"))
qrCode, has := html.Find("#start-auth-qrcode").Attr("src")
a.True(has, "should have the inline image data")
a.True(strings.HasPrefix(qrCode, "data:image/png;base64,"))
// TODO: decode image data and check qr code(?)
// simulate muxrpc client
testToken := "our-test-token"
ts.AuthWithSSB.CheckTokenReturns(23, nil)
go func() {
time.Sleep(4 * time.Second)
err = ts.SignalBridge.SessionWorked(serverChallenge, testToken)
r.NoError(err)
}()
// start reading sse
sseURL := urlTo(router.AuthWithSSBServerEvents, "sc", serverChallenge)
resp = ts.Client.GetBody(sseURL.String())
a.Equal(http.StatusOK, resp.Result().StatusCode)
// check contents of sse channel
sseBody := resp.Body.String()
a.True(strings.Contains(sseBody, "data: Waiting for solution"), "ping data")
a.True(strings.Contains(sseBody, "event: ping\n"), "ping event")
wantDataToken := fmt.Sprintf("data: %s\n", testToken)
a.True(strings.Contains(sseBody, wantDataToken), "token data")
a.True(strings.Contains(sseBody, "event: success\n"), "success event")
// use the token and go to /withssb/finalize and get a cookie
// (this happens in the browser engine via login-events.js)
finalizeURL := urlTo(router.AuthWithSSBFinalize, "token", testToken)
finalizeURL.Host = "localhost"
finalizeURL.Scheme = "https"
resp = ts.Client.GetBody(finalizeURL.String())
csrfCookie := resp.Result().Cookies()
a.Len(csrfCookie, 2, "csrf and session cookie")
// very cheap "browser" client session
jar, err := cookiejar.New(nil)
r.NoError(err)
jar.SetCookies(finalizeURL, csrfCookie)
// now request the protected dashboard page
dashboardURL, err := ts.Router.Get(router.AdminDashboard).URL()
r.Nil(err)
dashboardURL.Host = "localhost"
dashboardURL.Scheme = "https"
// load the cookie for the dashboard
cs := jar.Cookies(dashboardURL)
r.True(len(cs) > 0, "expecting one cookie!")
var sessionHeader = http.Header(map[string][]string{})
for _, c := range cs {
theCookie := c.String()
a.NotEqual("", theCookie, "should have a new cookie")
sessionHeader.Add("Cookie", theCookie)
}
ts.Client.SetHeaders(sessionHeader)
html, resp = ts.Client.GetHTML(dashboardURL.String())
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
t.Log(html.Find("body").Text())
}
webassert.Localized(t, html, []webassert.LocalizedElement{
{"#welcome", "AdminDashboardWelcome"},
{"title", "AdminDashboardTitle"},
})
}
func TestAuthWithSSBServerInitWrongSolution(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
// the keypair for our client
testMember := roomdb.Member{ID: 1234, Nickname: "test-member"}
client, err := keys.NewKeyPair(nil)
r.NoError(err)
testMember.PubKey = client.Feed
// setup the mocked database
ts.MembersDB.GetByFeedReturns(testMember, nil)
// prepare the url
urlTo := web.NewURLTo(ts.Router)
signInStartURL := urlTo(router.AuthWithSSBLogin,
"cid", client.Feed.Ref(),
)
r.NotNil(signInStartURL)
html, resp := ts.Client.GetHTML(signInStartURL.String())
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
t.Log(html.Find("body").Text())
}
serverChallenge, has := html.Find("#challenge").Attr("data-sc")
a.True(has, "should have server challenge")
a.NotEqual("", serverChallenge)
// simulate muxrpc client
ts.AuthWithSSB.CheckTokenReturns(-1, roomdb.ErrNotFound)
go func() {
time.Sleep(4 * time.Second)
err = ts.SignalBridge.SessionFailed(serverChallenge, fmt.Errorf("wrong solution"))
r.NoError(err)
}()
// start reading sse
sseURL := urlTo(router.AuthWithSSBServerEvents, "sc", serverChallenge)
resp = ts.Client.GetBody(sseURL.String())
a.Equal(http.StatusOK, resp.Result().StatusCode)
// check contents of sse channel
sseBody := resp.Body.String()
a.True(strings.Contains(sseBody, "data: Waiting for solution"), "ping data")
a.True(strings.Contains(sseBody, "event: ping\n"), "ping event")
a.True(strings.Contains(sseBody, "data: wrong solution\n"), "reason data")
a.True(strings.Contains(sseBody, "event: failed\n"), "success event")
// use an invalid token
finalizeURL := urlTo(router.AuthWithSSBFinalize, "token", "wrong")
resp = ts.Client.GetBody(finalizeURL.String())
a.Equal(http.StatusForbidden, resp.Result().StatusCode)
}

View File

@ -11,8 +11,7 @@ import (
"strconv"
"time"
refs "go.mindeco.de/ssb-refs"
"github.com/go-kit/kit/log/level"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions"
"github.com/russross/blackfriday/v2"
@ -20,7 +19,9 @@ import (
"go.mindeco.de/http/render"
"go.mindeco.de/logging"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
@ -29,6 +30,7 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
"github.com/ssb-ngi-pointer/go-ssb-room/web/members"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
refs "go.mindeco.de/ssb-refs"
)
var HTMLTemplates = []string{
@ -37,6 +39,7 @@ var HTMLTemplates = []string{
"aliases-resolved.html",
"invite/accept.tmpl",
"invite/consumed.tmpl",
"auth/fallback_sign_in.tmpl",
"notice/list.tmpl",
"notice/show.tmpl",
"error.tmpl",
@ -46,6 +49,7 @@ var HTMLTemplates = []string{
type Databases struct {
Aliases roomdb.AliasesService
AuthFallback roomdb.AuthFallbackService
AuthWithSSB roomdb.AuthWithSSBService
DeniedKeys roomdb.DeniedKeysService
Invites roomdb.InvitesService
Notices roomdb.NoticesService
@ -69,8 +73,9 @@ func New(
repo repo.Interface,
netInfo NetworkInfo,
roomState *roomstate.Manager,
roomEndpoints network.Endpoints,
bridge *signinwithssb.SignalBridge,
dbs Databases,
) (http.Handler, error) {
m := router.CompleteApp()
@ -147,7 +152,7 @@ func New(
return nil, err
}
store := &sessions.CookieStore{
cookieStore := &sessions.CookieStore{
Codecs: cookieCodec,
Options: &sessions.Options{
Path: "/",
@ -197,8 +202,8 @@ func New(
}, nil
})
a, err := auth.NewHandler(dbs.AuthFallback,
auth.SetStore(store),
authWithPassword, err := auth.NewHandler(dbs.AuthFallback,
auth.SetStore(cookieStore),
auth.SetErrorHandler(authErrH),
auth.SetNotAuthorizedHandler(notAuthorizedH),
auth.SetLifetime(2*time.Hour), // TODO: configure
@ -225,8 +230,37 @@ func New(
// TODO: explain problem between gorilla/mux named routers and authentication
mainMux := &http.ServeMux{}
// hookup handlers to the router
roomsAuth.Handler(m, r, a)
// start hooking up handlers to the router
authWithSSB := roomsAuth.NewWithSSBHandler(
m,
r,
netInfo.RoomID,
roomEndpoints,
dbs.Aliases,
dbs.Members,
dbs.AuthWithSSB,
cookieStore,
bridge,
)
m.Get(router.AuthLogin).Handler(r.StaticHTML("auth/decide_method.tmpl"))
m.Get(router.AuthFallbackFinalize).HandlerFunc(authWithPassword.Authorize)
m.Get(router.AuthFallbackLogin).Handler(r.HTML("auth/fallback_sign_in.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
return map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(req),
}, nil
}))
m.Get(router.AuthLogout).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
err = authWithSSB.Logout(w, req)
if err != nil {
level.Warn(logging.FromContext(req.Context())).Log("err", err)
}
authWithPassword.Logout(w, req)
})
adminHandler := admin.Handler(
netInfo.Domain,
@ -241,7 +275,7 @@ func New(
PinnedNotices: dbs.PinnedNotices,
},
)
mainMux.Handle("/admin/", a.Authenticate(adminHandler))
mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler))
m.Get(router.CompleteIndex).Handler(r.HTML("landing/index.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
notice, err := dbs.PinnedNotices.Get(req.Context(), roomdb.NoticeDescription, "en-GB")
@ -297,7 +331,7 @@ func New(
// apply HTTP middleware
middlewares := []func(http.Handler) http.Handler{
logging.InjectHandler(logger),
members.ContextInjecter(dbs.Members, a),
members.ContextInjecter(dbs.Members, authWithPassword, authWithSSB),
CSRF,
}

View File

@ -49,12 +49,11 @@ func (h inviteHandler) consume(rw http.ResponseWriter, req *http.Request) (inter
}
alias := req.FormValue("alias")
token := req.FormValue("token")
newMember, err := refs.ParseFeedRef(req.FormValue("new_member"))
if err != nil {
return nil, weberrors.ErrBadRequest{Where: "form data", Details: err}
return nil, weberrors.ErrBadRequest{Where: "new_member", Details: err}
}
inv, err := h.invites.Consume(req.Context(), token, *newMember)

View File

@ -61,6 +61,8 @@ func TestNoticesEditButtonVisible(t *testing.T) {
urlTo := web.NewURLTo(ts.Router)
ts.AliasesDB.ResolveReturns(roomdb.Alias{}, roomdb.ErrNotFound)
noticeData := roomdb.Notice{
ID: 42,
Title: "Welcome!",
@ -89,7 +91,7 @@ func TestNoticesEditButtonVisible(t *testing.T) {
// when dealing with cookies we also need to have an Host and URL-Scheme
// for the jar to save and load them correctly
formEndpoint := urlTo(router.AuthFallbackSignInForm)
formEndpoint := urlTo(router.AuthFallbackLogin)
r.NotNil(formEndpoint)
formEndpoint.Host = "localhost"
formEndpoint.Scheme = "https"
@ -126,7 +128,7 @@ func TestNoticesEditButtonVisible(t *testing.T) {
ts.AuthFallbackDB.CheckReturns(testUser.ID, nil)
ts.MembersDB.GetByIDReturns(testUser, nil)
postEndpoint, err := ts.Router.Get(router.AuthFallbackSignIn).URL()
postEndpoint, err := ts.Router.Get(router.AuthFallbackFinalize).URL()
r.Nil(err)
postEndpoint.Host = "localhost"
postEndpoint.Scheme = "https"

View File

@ -16,14 +16,16 @@ import (
"github.com/gorilla/mux"
"go.mindeco.de/http/tester"
"go.mindeco.de/logging/logtest"
refs "go.mindeco.de/ssb-refs"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network/mocked"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
refs "go.mindeco.de/ssb-refs"
)
type testSession struct {
@ -34,6 +36,7 @@ type testSession struct {
// mocked dbs
AuthDB *mockdb.FakeAuthWithSSBService
AuthFallbackDB *mockdb.FakeAuthFallbackService
AuthWithSSB *mockdb.FakeAuthWithSSBService
AliasesDB *mockdb.FakeAliasesService
MembersDB *mockdb.FakeMembersService
InvitesDB *mockdb.FakeInvitesService
@ -42,12 +45,17 @@ type testSession struct {
RoomState *roomstate.Manager
MockedEndpoints *mocked.FakeEndpoints
SignalBridge *signinwithssb.SignalBridge
NetworkInfo NetworkInfo
}
var testI18N = justTheKeys()
func setup(t *testing.T) *testSession {
t.Parallel()
var ts testSession
testRepoPath := filepath.Join("testrun", t.Name())
@ -63,6 +71,7 @@ func setup(t *testing.T) *testSession {
ts.AuthDB = new(mockdb.FakeAuthWithSSBService)
ts.AuthFallbackDB = new(mockdb.FakeAuthFallbackService)
ts.AuthWithSSB = new(mockdb.FakeAuthWithSSBService)
ts.AliasesDB = new(mockdb.FakeAliasesService)
ts.MembersDB = new(mockdb.FakeMembersService)
ts.InvitesDB = new(mockdb.FakeInvitesService)
@ -74,6 +83,8 @@ func setup(t *testing.T) *testSession {
ts.PinnedDB.GetReturns(defaultNotice, nil)
ts.NoticeDB = new(mockdb.FakeNoticesService)
ts.MockedEndpoints = new(mocked.FakeEndpoints)
ts.NetworkInfo = NetworkInfo{
Domain: "localhost",
PortMUXRPC: 8008,
@ -91,14 +102,19 @@ func setup(t *testing.T) *testSession {
ts.Router = router.CompleteApp()
ts.SignalBridge = signinwithssb.NewSignalBridge()
h, err := New(
log,
testRepo,
ts.NetworkInfo,
ts.RoomState,
ts.MockedEndpoints,
ts.SignalBridge,
Databases{
Aliases: ts.AliasesDB,
AuthFallback: ts.AuthFallbackDB,
AuthWithSSB: ts.AuthWithSSB,
Members: ts.MembersDB,
Invites: ts.InvitesDB,
Notices: ts.NoticeDB,

View File

@ -4,6 +4,7 @@ GenericSave = "Save"
GenericCreate = "Create"
GenericPreview = "Preview"
GenericLanguage = "Language"
GenericOpenLink = "Open Link"
PageNotFound = "The requested page was not found."
@ -14,11 +15,22 @@ RoleAdmin = "Admin"
LandingTitle = "ohai my room"
LandingWelcome = "Landing welcome here"
AuthFallbackWelcome = "You really shouldn't be here.... Let's get you through this."
AuthFallbackTitle = "The place of last resort"
AuthSignIn = "Sign in"
AuthSignOut = "Sign out"
AuthTitle = "Member Authentication"
AuthWelcome = "If you are a member of this room, you can access the internal dashboard. Click on your preferred sign-in method below:"
AuthWithSSBTitle = "Sign in with SSB"
AuthWithSSBInstruct = "Easy and secure method, if your SSB app supports it."
AuthWithSSBWelcome = "To sign-in with your SSB identity stored on this device, press the button below which will open a compatible SSB app, if it's installed."
AuthWithSSBInstructQR = "If your SSB app is on another device, you can scan the following QR code to sign-in with that device's SSB identity."
AuthWithSSBError = "Sign-in failed. Please make sure you use an SSB app that supports this method of login, and click the button above within a minute after this page was opened."
AuthFallbackTitle = "Password sign-in"
AuthFallbackWelcome = "Signing in with username and password is only possible if the administrator has given you one, because we do not support user registration."
AuthFallbackInstruct = "This method is an acceptable fallback, if you have a username and password."
AdminDashboardWelcome = "Welcome to your dashboard"
AdminDashboardTitle = "Room Admin Dashboard"

View File

@ -1,3 +1,5 @@
// SPDX-License-Identifier: MIT
// Package members implements helpers for accessing the currently logged in admin or moderator of an active request.
package members
@ -5,45 +7,80 @@ import (
"context"
"net/http"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"go.mindeco.de/http/auth"
"go.mindeco.de/http/render"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
authWithSSB "github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/auth"
)
type roomMemberContextKeyType string
var roomMemberContextKey roomMemberContextKeyType = "ssb:room:httpcontext:member"
type Middleware func(next http.Handler) http.Handler
// AuthenticateFromContext calls the next http handler if there is a member stored in the context
// otherwise it will call r.Error
func AuthenticateFromContext(r *render.Renderer) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if FromContext(req.Context()) == nil {
r.Error(w, req, http.StatusUnauthorized, weberrors.ErrNotAuthorized)
return
}
next.ServeHTTP(w, req)
})
}
}
// FromContext returns the member or nil if not logged in
func FromContext(ctx context.Context) *roomdb.Member {
v := ctx.Value(roomMemberContextKey)
m, ok := v.(roomdb.Member)
m, ok := v.(*roomdb.Member)
if !ok {
return nil
}
return &m
return m
}
// ContextInjecter returns middleware for injecting a member into the context of the request.
// Retreive it using FromContext(ctx)
func ContextInjecter(mdb roomdb.MembersService, a *auth.Handler) func(http.Handler) http.Handler {
func ContextInjecter(mdb roomdb.MembersService, withPassword *auth.Handler, withSSB *authWithSSB.WithSSBHandler) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
v, err := a.AuthenticateRequest(req)
if err != nil {
next.ServeHTTP(w, req)
return
var (
member *roomdb.Member
errWithPassword, errWithSSB error
)
v, errWithPassword := withPassword.AuthenticateRequest(req)
if errWithPassword == nil {
mid, ok := v.(int64)
if !ok {
next.ServeHTTP(w, req)
return
}
m, err := mdb.GetByID(req.Context(), mid)
if err != nil {
next.ServeHTTP(w, req)
return
}
member = &m
}
mid, ok := v.(int64)
if !ok {
next.ServeHTTP(w, req)
return
m, errWithSSB := withSSB.AuthenticateRequest(req)
if errWithSSB == nil {
member = m
}
member, err := mdb.GetByID(req.Context(), mid)
if err != nil {
// if both methods failed, don't update the context
if errWithPassword != nil && errWithSSB != nil {
next.ServeHTTP(w, req)
return
}

View File

@ -1,3 +1,5 @@
// SPDX-License-Identifier: MIT
package members
import (
@ -14,7 +16,7 @@ import (
func MiddlewareForTests(m roomdb.Member) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := context.WithValue(req.Context(), roomMemberContextKey, m)
ctx := context.WithValue(req.Context(), roomMemberContextKey, &m)
next.ServeHTTP(w, req.WithContext(ctx))
})
}

View File

@ -6,27 +6,32 @@ import "github.com/gorilla/mux"
// constant names for the named routes
const (
AuthFallbackSignInForm = "auth:fallback:signin:form"
AuthFallbackSignIn = "auth:fallback:signin"
AuthFallbackSignOut = "auth:fallback:logout"
AuthLogin = "auth:login"
AuthLogout = "auth:logout"
AuthWithSSBSignIn = "auth:ssb:signin"
AuthWithSSBSignOut = "auth:ssb:logout"
AuthFallbackLogin = "auth:fallback:login"
AuthFallbackFinalize = "auth:fallback:finalize"
AuthWithSSBLogin = "auth:withssb:login"
AuthWithSSBServerEvents = "auth:withssb:sse"
AuthWithSSBFinalize = "auth:withssb:finalize"
)
// NewSignin constructs a mux.Router containing the routes for sign-in and -out
// Auth constructs a mux.Router containing the routes for sign-in and -out
func Auth(m *mux.Router) *mux.Router {
if m == nil {
m = mux.NewRouter()
}
// register fallback
m.Path("/fallback/signin").Methods("GET").Name(AuthFallbackSignInForm)
m.Path("/fallback/signin").Methods("POST").Name(AuthFallbackSignIn)
m.Path("/fallback/logout").Methods("GET").Name(AuthFallbackSignOut)
m.Path("/login").Methods("GET").Name(AuthLogin)
m.Path("/logout").Methods("GET").Name(AuthLogout)
m.Path("/withssb/signin").Methods("GET").Name(AuthWithSSBSignIn)
m.Path("/withssb/logout").Methods("GET").Name(AuthWithSSBSignOut)
m.Path("/fallback/login").Methods("GET").Name(AuthFallbackLogin)
m.Path("/fallback/finalize").Methods("POST").Name(AuthFallbackFinalize)
m.Path("/withssb/login").Methods("GET").Name(AuthWithSSBLogin)
m.Path("/withssb/events").Methods("GET").Name(AuthWithSSBServerEvents)
m.Path("/withssb/finalize").Methods("GET").Name(AuthWithSSBFinalize)
return m
}

View File

@ -24,13 +24,13 @@ const (
func CompleteApp() *mux.Router {
m := mux.NewRouter()
Auth(m.PathPrefix("/auth").Subrouter())
Auth(m)
Admin(m.PathPrefix("/admin").Subrouter())
m.Path("/").Methods("GET").Name(CompleteIndex)
m.Path("/about").Methods("GET").Name(CompleteAbout)
m.Path("/{alias}").Methods("GET").Name(CompleteAliasResolve)
m.Path("/alias/{alias}").Methods("GET").Name(CompleteAliasResolve)
m.Path("/invite/accept").Methods("GET").Name(CompleteInviteAccept)
m.Path("/invite/consume").Methods("POST").Name(CompleteInviteConsume)

View File

@ -1,11 +1,11 @@
{{ define "title" }}{{i18n "AdminAllowListRemoveConfirmTitle"}}{{ end }}
{{ define "title" }}{{i18n "AdminMembersRemoveConfirmTitle"}}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center h-64">
<span
id="welcome"
class="text-center"
>{{i18n "AdminAllowListRemoveConfirmWelcome"}}</span>
>{{i18n "AdminMembersRemoveConfirmWelcome"}}</span>
<pre
id="verify"

View File

@ -3,7 +3,7 @@
<div>
<h1>{{.Alias.Name}}</h1>
<pre>{{.RoomAddr}}</pre>
<p class="color-red-600">TODO: ssb-uri</p>
<pre>{{.SSBURI}}</pre>
<a href="{{.SSBURI}}">Consume</a>
</div>
{{end}}

View File

@ -0,0 +1,30 @@
{{ define "title" }}{{i18n "AuthTitle"}}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center self-center max-w-lg">
<span class="text-center mt-8">{{i18n "AuthWelcome"}}</span>
</div>
<div class="flex flex-col sm:flex-row justify-center items-center sm:items-stretch">
<a
href="{{urlTo "auth:withssb:login"}}"
class="w-64 sm:mr-4 my-6 py-10 border-green-200 border-2 rounded-3xl flex flex-col justify-start items-center hover:border-green-400 hover:shadow-xl transition"
>
<svg class="w-12 h-12 text-green-500 mb-4" viewBox="0 0 24 24">
<path fill="currentColor" d="M12.66 13.67C12.32 14 11.93 14.29 11.5 14.5V21L9.5 23L7.5 21L9.5 19.29L8 18L9.5 16.71L7.5 15V14.5C6 13.77 5 12.26 5 10.5C5 8 7 6 9.5 6C9.54 6 9.58 6 9.61 6C9.59 6.07 9.54 6.12 9.5 6.18C9.23 6.79 9.08 7.43 9.03 8.08C8.43 8.28 8 8.84 8 9.5C8 10.33 8.67 11 9.5 11C9.53 11 9.57 11 9.6 11C10.24 12.25 11.34 13.2 12.66 13.67M16 6C16 5.37 15.9 4.75 15.72 4.18C17.06 4.56 18.21 5.55 18.73 6.96C19.33 8.62 18.89 10.39 17.75 11.59L20 17.68L18.78 20.25L16.22 19.05L17.5 16.76L15.66 16.06L16.63 14.34L14.16 13.41L14 12.95C12.36 12.77 10.88 11.7 10.27 10.04C9.42 7.71 10.63 5.12 12.96 4.27C13.14 4.21 13.33 4.17 13.5 4.13C12.84 2.87 11.53 2 10 2C7.79 2 6 3.79 6 6C6 6.09 6 6.17 6.03 6.26C5.7 6.53 5.4 6.82 5.15 7.15C5.06 6.78 5 6.4 5 6C5 3.24 7.24 1 10 1S15 3.24 15 6C15 7.16 14.6 8.21 13.94 9.06C16.08 8.88 16 6 16 6M12.81 8.1C12.87 8.27 12.96 8.41 13.06 8.54C13.62 7.88 13.97 7.04 14 6.11C13.89 6.13 13.8 6.15 13.7 6.18C12.92 6.47 12.5 7.33 12.81 8.1Z" />
</svg>
<h1 class="text-xl font-bold text-green-500">{{i18n "AuthWithSSBTitle"}}</h1>
<span class="mx-3 mt-2 text-center text-sm">{{i18n "AuthWithSSBInstruct"}}</span>
</a>
<a
href="{{urlTo "auth:fallback:login"}}"
class="w-64 sm:ml-4 my-6 py-10 border-gray-200 border-2 rounded-3xl flex flex-col justify-start items-center hover:border-gray-400 hover:shadow-xl transition"
>
<svg class="w-12 h-12 text-gray-500 mb-4" viewBox="0 0 24 24">
<path fill="currentColor" d="M17,7H22V17H17V19A1,1 0 0,0 18,20H20V22H17.5C16.95,22 16,21.55 16,21C16,21.55 15.05,22 14.5,22H12V20H14A1,1 0 0,0 15,19V5A1,1 0 0,0 14,4H12V2H14.5C15.05,2 16,2.45 16,3C16,2.45 16.95,2 17.5,2H20V4H18A1,1 0 0,0 17,5V7M2,7H13V9H4V15H13V17H2V7M20,15V9H17V15H20M8.5,12A1.5,1.5 0 0,0 7,10.5A1.5,1.5 0 0,0 5.5,12A1.5,1.5 0 0,0 7,13.5A1.5,1.5 0 0,0 8.5,12M13,10.89C12.39,10.33 11.44,10.38 10.88,11C10.32,11.6 10.37,12.55 11,13.11C11.55,13.63 12.43,13.63 13,13.11V10.89Z" />
</svg>
<h1 class="text-xl font-bold text-gray-500">{{i18n "AuthFallbackTitle"}}</h1>
<span class="mx-3 mt-2 text-center text-sm">{{i18n "AuthFallbackInstruct"}}</span>
</a>
</div>
{{end}}

View File

@ -1,29 +1,25 @@
{{ define "title" }}{{i18n "AuthFallbackTitle"}}{{ end }}
{{ define "title" }}{{i18n "AuthTitle"}}{{ end }}
{{ define "content" }}
<div id="page-header">
<h1 id="welcome" class="text-lg">{{i18n "AuthFallbackWelcome"}}</h1>
</div>
<div>
<form method="POST" action={{urlTo "auth:fallback:signin" }} class="flex flex-row items-end">
{{ .csrfField }}
<div class="w-96 grid grid-cols-2 gap-x-4 gap-y-1 mr-4">
<label>Username</label>
<label>Password</label>
<input
type="text"
name="user"
class="shadow rounded border border-transparent h-8 p-1 focus:outline-none focus:ring-2 focus:ring-pink-400 focus:border-transparent"
>
<input
type="password"
name="pass"
class="shadow rounded border border-transparent h-8 p-1 focus:outline-none focus:ring-2 focus:ring-pink-400 focus:border-transparent"
>
</div>
<button
type="submit"
class="shadow rounded px-4 h-8 text-gray-100 bg-pink-600 hover:bg-pink-700 focus:outline-none focus:ring-2 focus:ring-pink-600 focus:ring-opacity-50"
>Enter</button>
</form>
</div>
<div class="flex flex-col justify-center items-center self-center max-w-lg">
<span id="welcome" class="text-center mt-8">{{i18n "AuthFallbackWelcome"}}</span>
<form
id="password-fallback"
method="POST"
action="{{urlTo "auth:fallback:finalize"}}"
class="flex flex-row items-end"
>
{{ .csrfField }}
<div class="flex flex-col w-48">
<label class="mt-8 text-sm text-gray-600">Username</label>
<input type="text" name="user"
class="shadow rounded border border-transparent h-8 p-1 focus:outline-none focus:ring-2 focus:ring-green-400 focus:border-transparent">
<label class="mt-8 text-sm text-gray-600">Password</label>
<input type="password" name="pass"
class="shadow rounded border border-transparent h-8 p-1 focus:outline-none focus:ring-2 focus:ring-green-400 focus:border-transparent">
<button type="submit"
class="my-8 shadow rounded px-4 h-8 text-gray-100 bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-600 focus:ring-opacity-50">Enter</button>
</div>
</form>
</div>
{{end}}

View File

@ -0,0 +1,33 @@
{{ define "title" }}{{i18n "AuthWithSSBTitle"}}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center self-center max-w-lg">
<span id="welcome" class="text-center mt-8">{{i18n "AuthWithSSBWelcome"}}</span>
<a
id="start-auth-uri"
href="{{.SSBURI}}"
target="_blank"
class="shadow rounded flex flex-row justify-center items-center mt-8 px-4 h-8 text-gray-100 bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-600 focus:ring-opacity-50"
>{{i18n "AuthWithSSBTitle"}}</a>
<p id="waiting" class="hidden mt-8 animate-pulse text-green-500">Waiting for confirmation</p>
<p id="failed" class="hidden mt-8 text-red-700 text-center">{{i18n "AuthWithSSBError"}}</p>
<hr class="mt-8 w-64 h-px bg-gray-200"></hr>
<span class="text-center mt-8">{{i18n "AuthWithSSBInstructQR"}}</span>
<img
id="start-auth-qrcode"
src="{{.QRCodeURI}}"
alt="QR-Code to pass the challenge to an App"
width="160"
height="160"
class="mt-8"
/>
</div>
<div id="challenge" class="hidden" data-sc="{{.ServerChallenge}}"></div>
<script src="/assets/login-events.js"></script>
{{end}}

View File

@ -31,17 +31,17 @@
<svg class="text-green-500 w-4 h-4 mr-1" viewBox="0 0 24 24">
<path fill="currentColor" d="M22,18V22H18V19H15V16H12L9.74,13.74C9.19,13.91 8.61,14 8,14A6,6 0 0,1 2,8A6,6 0 0,1 8,2A6,6 0 0,1 14,8C14,8.61 13.91,9.19 13.74,9.74L22,18M7,5A2,2 0 0,0 5,7A2,2 0 0,0 7,9A2,2 0 0,0 9,7A2,2 0 0,0 7,5Z" />
</svg>
<span class="text-green-500 text-sm">{{$user.Nickname}}</span>
<span class="text-green-500 text-sm truncate w-32">{{$user.Nickname}} {{$user.PubKey.Ref}}</span>
</div>
<a
href="{{urlTo "auth:fallback:logout"}}"
href="{{urlTo "auth:logout"}}"
class="pl-3 pr-4 py-2 sm:py-1 font-semibold text-sm text-gray-500 hover:text-red-600"
>{{i18n "AuthSignOut"}}</a>
</span>
{{else}}
<a
href="{{urlTo "auth:fallback:signin:form"}}"
class="pl-3 pr-4 py-2 sm:py-1 font-semibold text-sm text-gray-500 hover:text-green-600"
href="{{urlTo "auth:login"}}"
class="pl-3 pr-4 py-2 sm:py-1 font-semibold text-sm text-gray-500 hover:text-green-500"
>{{i18n "AuthSignIn"}}</a>
{{end}}
</div>

View File

@ -18,7 +18,7 @@ type LocalizedElement struct {
func Localized(t *testing.T, html *goquery.Document, elems []LocalizedElement) {
a := assert.New(t)
for i, pair := range elems {
a.Equal(pair.Label, html.Find(pair.Selector).Text(), "localized pair %d failed", i+1)
a.Equal(pair.Label, html.Find(pair.Selector).Text(), "localized pair %d failed (selector: %s)", i+1, pair.Selector)
}
}