begin sign-in with ssb
* sketch session store * use session store and unify authentication handling * sketch muxrpc handlers
This commit is contained in:
parent
b5e2ea6572
commit
a180c74c38
|
@ -248,9 +248,11 @@ func runroomsrv() error {
|
|||
RoomID: roomsrv.Whoami(),
|
||||
},
|
||||
roomsrv.StateManager,
|
||||
roomsrv.Network,
|
||||
handlers.Databases{
|
||||
Aliases: db.Aliases,
|
||||
AuthFallback: db.AuthFallback,
|
||||
AuthWithSSB: db.AuthWithSSB,
|
||||
DeniedKeys: db.DeniedKeys,
|
||||
Invites: db.Invites,
|
||||
Notices: db.Notices,
|
||||
|
|
5
go.mod
5
go.mod
|
@ -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,7 +15,6 @@ 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
|
||||
|
@ -30,7 +29,7 @@ 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/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
|
|
11
go.sum
11
go.sum
|
@ -232,7 +232,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=
|
||||
|
@ -427,18 +426,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=
|
||||
|
@ -473,8 +468,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=
|
||||
|
|
|
@ -24,13 +24,19 @@ type EndpointStat struct {
|
|||
Endpoint muxrpc.Endpoint
|
||||
}
|
||||
|
||||
//go:generate counterfeiter -o mocked/endpoints.go . Endpoints
|
||||
|
||||
type Endpoints interface {
|
||||
GetEndpointFor(refs.FeedRef) (muxrpc.Endpoint, bool)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func GenerateChallenge() string {
|
||||
buf := make([]byte, challengeLength)
|
||||
rand.Read(buf)
|
||||
return base64.URLEncoding.EncodeToString(buf)
|
||||
}
|
||||
|
||||
// this structure is used to verify an incoming client response
|
||||
type ClientRequest struct {
|
||||
ClientID, ServerID refs.FeedRef
|
||||
|
||||
ClientChallenge string
|
||||
ServerChallenge string
|
||||
}
|
||||
|
||||
// recreate the signed message
|
||||
func (cr ClientRequest) 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()
|
||||
}
|
||||
|
||||
func (cr ClientRequest) Sign(privateKey ed25519.PrivateKey) []byte {
|
||||
msg := cr.createMessage()
|
||||
return ed25519.Sign(privateKey, msg)
|
||||
}
|
||||
|
||||
func (cr ClientRequest) Validate(signature []byte) bool {
|
||||
msg := cr.createMessage()
|
||||
return ed25519.Verify(cr.ClientID.PubKey(), msg, signature)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package signinwithssb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
)
|
||||
|
||||
func TestClientRequestString(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 ClientRequest
|
||||
|
||||
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))
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package signinwithssb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"go.cryptoscope.co/muxrpc/v2"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
|
||||
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 alias registration and recvocation
|
||||
type Handler struct {
|
||||
logger kitlog.Logger
|
||||
self refs.FeedRef
|
||||
|
||||
sessions roomdb.AuthWithSSBService
|
||||
members roomdb.MembersService
|
||||
|
||||
roomDomain string // the http(s) domain of the room to signal redirect addresses
|
||||
}
|
||||
|
||||
// New returns a fresh alias muxrpc handler
|
||||
func New(
|
||||
log kitlog.Logger,
|
||||
self refs.FeedRef,
|
||||
sessiondb roomdb.AuthWithSSBService,
|
||||
membersdb roomdb.MembersService,
|
||||
roomDomain string) Handler {
|
||||
|
||||
var h Handler
|
||||
h.self = self
|
||||
h.roomDomain = roomDomain
|
||||
h.logger = log
|
||||
h.sessions = sessiondb
|
||||
h.members = membersdb
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h Handler) SendSolution(ctx context.Context, req *muxrpc.Request) (interface{}, error) {
|
||||
clientID, err := network.GetFeedRefFromAddr(req.RemoteAddr())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var params []string
|
||||
if err := json.Unmarshal(req.RawArgs, ¶ms); 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 sol validate.ClientRequest
|
||||
sol.ServerID = h.self
|
||||
sol.ServerChallenge = params[0]
|
||||
sol.ClientID = *clientID
|
||||
sol.ClientChallenge = params[1]
|
||||
|
||||
sig, err := base64.StdEncoding.DecodeString(params[2])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sc is not valid base64 data: %w", err)
|
||||
}
|
||||
|
||||
if !sol.Validate(sig) {
|
||||
return nil, fmt.Errorf("not a valid solution")
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// h.challenges.Solved(sc)
|
||||
// return true, nil
|
||||
|
||||
return nil, fmt.Errorf("TODO: update SSE")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
member, err := h.members.GetByFeed(ctx, *clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = h.sessions.WipeTokensForMember(ctx, member.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
|
@ -32,9 +32,21 @@ 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)
|
||||
|
||||
// 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 {
|
||||
|
|
|
@ -2,19 +2,258 @@
|
|||
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
|
||||
}
|
||||
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) 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.wipeTokensForMemberMutex.RLock()
|
||||
defer fake.wipeTokensForMemberMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,107 @@ 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
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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" )
|
||||
);
|
||||
CREATE UNIQUE INDEX SIWSSB_by_token ON SIWSSB_sessions(token);
|
||||
CREATE UNIQUE 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
|
@ -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) }
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -4,6 +4,7 @@ package roomsrv
|
|||
|
||||
import (
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/muxrpc/handlers/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
muxrpc "go.cryptoscope.co/muxrpc/v2"
|
||||
"go.cryptoscope.co/muxrpc/v2/typemux"
|
||||
|
@ -31,6 +32,10 @@ func (s *Server) initHandlers(aliasDB roomdb.AliasesService) {
|
|||
s.domain,
|
||||
)
|
||||
|
||||
siwssbHandler := signinwithssb.New(
|
||||
kitlog.With(s.logger, "unit", "auth-with-ssb"),
|
||||
)
|
||||
|
||||
// register muxrpc commands
|
||||
registries := []typemux.HandlerMux{s.public, s.master}
|
||||
|
||||
|
@ -47,5 +52,10 @@ 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))
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -15,13 +15,19 @@ import (
|
|||
|
||||
var HTMLTemplates = []string{
|
||||
"auth/fallback_sign_in.tmpl",
|
||||
"auth/withssb_sign_in.tmpl",
|
||||
}
|
||||
|
||||
func Handler(m *mux.Router, r *render.Renderer, a *auth.Handler) http.Handler {
|
||||
func NewFallbackPasswordHandler(
|
||||
m *mux.Router,
|
||||
r *render.Renderer,
|
||||
ah *auth.Handler,
|
||||
) {
|
||||
if m == nil {
|
||||
m = router.Auth(nil)
|
||||
}
|
||||
|
||||
// just the form
|
||||
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),
|
||||
|
@ -29,8 +35,7 @@ func Handler(m *mux.Router, r *render.Renderer, a *auth.Handler) http.Handler {
|
|||
}))
|
||||
|
||||
// hook up the auth handler to the router
|
||||
m.Get(router.AuthFallbackSignIn).HandlerFunc(a.Authorize)
|
||||
m.Get(router.AuthFallbackSignOut).HandlerFunc(a.Logout)
|
||||
m.Get(router.AuthFallbackSignIn).HandlerFunc(ah.Authorize)
|
||||
|
||||
return m
|
||||
m.Get(router.AuthSignOut).HandlerFunc(ah.Logout)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,255 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"go.cryptoscope.co/muxrpc/v2"
|
||||
"go.mindeco.de/http/render"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// WithSSBHandler implements the oauth-like challenge/response dance described in
|
||||
// https://ssb-ngi-pointer.github.io/rooms2/#sign-in-with-ssb
|
||||
type WithSSBHandler struct {
|
||||
roomID refs.FeedRef
|
||||
|
||||
membersdb roomdb.MembersService
|
||||
aliasesdb roomdb.AliasesService
|
||||
sessiondb roomdb.AuthWithSSBService
|
||||
|
||||
cookieStore sessions.Store
|
||||
|
||||
endpoints network.Endpoints
|
||||
}
|
||||
|
||||
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,
|
||||
) *WithSSBHandler {
|
||||
|
||||
var ssb WithSSBHandler
|
||||
ssb.roomID = roomID
|
||||
ssb.aliasesdb = aliasDB
|
||||
ssb.membersdb = membersDB
|
||||
ssb.endpoints = endpoints
|
||||
ssb.sessiondb = sessiondb
|
||||
ssb.cookieStore = cookies
|
||||
|
||||
m.Get(router.AuthWithSSBSignIn).HandlerFunc(r.HTML("auth/withssb_sign_in.tmpl", ssb.login))
|
||||
|
||||
return &ssb
|
||||
}
|
||||
|
||||
func (h WithSSBHandler) login(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
queryParams := req.URL.Query()
|
||||
|
||||
var clientReq signinwithssb.ClientRequest
|
||||
clientReq.ServerID = h.roomID // fill in the server
|
||||
|
||||
// validate and update client challenge
|
||||
cc := queryParams.Get("cc")
|
||||
if _, err := signinwithssb.DecodeChallengeString(cc); err != nil {
|
||||
return nil, weberrors.ErrBadRequest{Where: "client-challenge", Details: err}
|
||||
}
|
||||
clientReq.ClientChallenge = cc
|
||||
|
||||
// check who the client is
|
||||
var client refs.FeedRef
|
||||
if cid := queryParams.Get("cid"); cid != "" {
|
||||
parsed, err := refs.ParseFeedRef(cid)
|
||||
if err != nil {
|
||||
return nil, weberrors.ErrBadRequest{Where: "cid", Details: err}
|
||||
}
|
||||
client = *parsed
|
||||
} else {
|
||||
alias, err := h.aliasesdb.Resolve(req.Context(), queryParams.Get("alias"))
|
||||
if err != nil {
|
||||
return nil, weberrors.ErrBadRequest{Where: "alias", Details: err}
|
||||
}
|
||||
client = alias.Feed
|
||||
}
|
||||
|
||||
// check that we have that member
|
||||
member, err := h.membersdb.GetByFeed(req.Context(), client)
|
||||
if err != nil {
|
||||
if err == roomdb.ErrNotFound {
|
||||
return nil, weberrors.ErrForbidden{Details: fmt.Errorf("sign-in: client isnt a member")}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
clientReq.ClientID = client
|
||||
|
||||
// get the connected client for that member
|
||||
edp, connected := h.endpoints.GetEndpointFor(client)
|
||||
if !connected {
|
||||
return nil, weberrors.ErrForbidden{Details: fmt.Errorf("sign-in: client not connected to room")}
|
||||
}
|
||||
|
||||
// roll a Challenge from the server
|
||||
sc := signinwithssb.GenerateChallenge()
|
||||
clientReq.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 nil, err
|
||||
}
|
||||
|
||||
// decode and validate the response
|
||||
solutionBytes, err := base64.URLEncoding.DecodeString(solution)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !clientReq.Validate(solutionBytes) {
|
||||
return nil, fmt.Errorf("sign-in with ssb: validation of client solution failed")
|
||||
}
|
||||
|
||||
// create a session for invalidation
|
||||
tok, err := h.sessiondb.CreateToken(req.Context(), member.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, err := h.cookieStore.Get(req, siwssbSessionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session.Values[memberToken] = tok
|
||||
session.Values[userTimeout] = time.Now().Add(lifetime)
|
||||
if err := session.Save(req, w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return "you are now logged in!", nil
|
||||
}
|
||||
|
||||
// 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 lifetime = time.Hour * 24
|
||||
|
||||
// Authenticate calls the next unless AuthenticateRequest returns an error
|
||||
func (h WithSSBHandler) Authenticate(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := h.AuthenticateRequest(r); err != nil {
|
||||
// TODO: render.Error
|
||||
http.Error(w, weberrors.ErrNotAuthorized.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// 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 ID 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) {
|
||||
session, err := h.cookieStore.Get(r, siwssbSessionName)
|
||||
if err != nil {
|
||||
// TODO: render.Error
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
// ah.errorHandler(w, r, err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
session.Values[userTimeout] = time.Now().Add(-lifetime)
|
||||
session.Options.MaxAge = -1
|
||||
if err := session.Save(r, w); err != nil {
|
||||
// TODO: render.Error
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
// ah.errorHandler(w, r, err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
|
@ -2,6 +2,8 @@ package handlers
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
|
@ -9,7 +11,12 @@ import (
|
|||
|
||||
"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"
|
||||
|
@ -53,7 +60,7 @@ 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)
|
||||
|
||||
|
@ -112,6 +119,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 +177,196 @@ func TestFallbackAuth(t *testing.T) {
|
|||
{"#roomCount", "AdminRoomCountPlural"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthWithSSBNotConnected(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.AuthWithSSBSignIn,
|
||||
"cid", client.Feed.Ref(),
|
||||
"challenge", 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 TestAuthWithSSBNotAllowed(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.AuthWithSSBSignIn,
|
||||
"cid", client.Feed.Ref(),
|
||||
"challenge", 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 TestAuthWithSSBHasClient(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 req signinwithssb.ClientRequest
|
||||
req.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
|
||||
req.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
|
||||
req.ServerChallenge = serverChallenge
|
||||
|
||||
clientChallenge, ok := args[1].(string)
|
||||
r.True(ok, "argument[1] is not a string: %T", args[1])
|
||||
a.Equal(req.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 := req.Sign(client.Pair.Secret)
|
||||
|
||||
*strptr = base64.URLEncoding.EncodeToString(clientSig)
|
||||
return nil
|
||||
})
|
||||
|
||||
// setup the fake client endpoint
|
||||
ts.MockedEndpoints.GetEndpointForReturns(&edp, true)
|
||||
|
||||
cc := signinwithssb.GenerateChallenge()
|
||||
// update the challenge
|
||||
req.ClientChallenge = cc
|
||||
|
||||
// prepare the url
|
||||
signInStartURL := web.NewURLTo(ts.Router)(router.AuthWithSSBSignIn,
|
||||
"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.StatusOK, resp.Code)
|
||||
|
||||
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, err := ts.Router.Get(router.AdminDashboard).URL()
|
||||
r.Nil(err)
|
||||
dashboardURL.Host = "localhost"
|
||||
dashboardURL.Scheme = "https"
|
||||
|
||||
var sessionHeader = http.Header(map[string][]string{})
|
||||
cs := jar.Cookies(dashboardURL)
|
||||
|
||||
r.True(len(cs) > 0, "expecting one cookie!")
|
||||
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"},
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -11,8 +11,6 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
|
@ -20,6 +18,7 @@ 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/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
|
||||
|
@ -29,6 +28,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{
|
||||
|
@ -46,6 +46,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 +70,8 @@ func New(
|
|||
repo repo.Interface,
|
||||
netInfo NetworkInfo,
|
||||
roomState *roomstate.Manager,
|
||||
roomEndpoints network.Endpoints,
|
||||
dbs Databases,
|
||||
|
||||
) (http.Handler, error) {
|
||||
m := router.CompleteApp()
|
||||
|
||||
|
@ -147,7 +148,7 @@ func New(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
store := &sessions.CookieStore{
|
||||
cookieStore := &sessions.CookieStore{
|
||||
Codecs: cookieCodec,
|
||||
Options: &sessions.Options{
|
||||
Path: "/",
|
||||
|
@ -197,8 +198,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 +226,21 @@ 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,
|
||||
)
|
||||
|
||||
// just hooks up the router to the handler
|
||||
roomsAuth.NewFallbackPasswordHandler(m, r, authWithPassword)
|
||||
|
||||
adminHandler := admin.Handler(
|
||||
netInfo.Domain,
|
||||
|
@ -241,7 +255,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 +311,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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"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/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb"
|
||||
|
@ -34,6 +35,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,6 +44,8 @@ type testSession struct {
|
|||
|
||||
RoomState *roomstate.Manager
|
||||
|
||||
MockedEndpoints *mocked.FakeEndpoints
|
||||
|
||||
NetworkInfo NetworkInfo
|
||||
}
|
||||
|
||||
|
@ -63,6 +67,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 +79,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,
|
||||
|
@ -96,9 +103,11 @@ func setup(t *testing.T) *testSession {
|
|||
testRepo,
|
||||
ts.NetworkInfo,
|
||||
ts.RoomState,
|
||||
ts.MockedEndpoints,
|
||||
Databases{
|
||||
Aliases: ts.AliasesDB,
|
||||
AuthFallback: ts.AuthFallbackDB,
|
||||
AuthWithSSB: ts.AuthWithSSB,
|
||||
Members: ts.MembersDB,
|
||||
Invites: ts.InvitesDB,
|
||||
Notices: ts.NoticeDB,
|
||||
|
|
|
@ -19,6 +19,9 @@ AuthFallbackTitle = "The place of last resort"
|
|||
AuthSignIn = "Sign in"
|
||||
AuthSignOut = "Sign out"
|
||||
|
||||
AuthWithSSBTitle = "Sign-in with SSB"
|
||||
AuthWithSSBWelcome = "If you have a compatible device/application, you can sign-in here without a password."
|
||||
|
||||
AdminDashboardWelcome = "Welcome to your dashboard"
|
||||
AdminDashboardTitle = "Room Admin Dashboard"
|
||||
|
||||
|
|
|
@ -5,45 +5,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.ErrBadRequest{})
|
||||
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
|
||||
}
|
||||
|
|
|
@ -14,7 +14,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))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,10 +8,10 @@ import "github.com/gorilla/mux"
|
|||
const (
|
||||
AuthFallbackSignInForm = "auth:fallback:signin:form"
|
||||
AuthFallbackSignIn = "auth:fallback:signin"
|
||||
AuthFallbackSignOut = "auth:fallback:logout"
|
||||
|
||||
AuthWithSSBSignIn = "auth:ssb:signin"
|
||||
AuthWithSSBSignOut = "auth:ssb:logout"
|
||||
AuthWithSSBSignIn = "auth:ssb:signin"
|
||||
|
||||
AuthSignOut = "auth:logout"
|
||||
)
|
||||
|
||||
// NewSignin constructs a mux.Router containing the routes for sign-in and -out
|
||||
|
@ -23,10 +23,10 @@ func Auth(m *mux.Router) *mux.Router {
|
|||
// 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("/withssb/signin").Methods("GET").Name(AuthWithSSBSignIn)
|
||||
m.Path("/withssb/logout").Methods("GET").Name(AuthWithSSBSignOut)
|
||||
|
||||
m.Path("/logout").Methods("GET").Name(AuthSignOut)
|
||||
|
||||
return m
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{{ define "title" }}{{i18n "AuthWithSSBTitle"}}{{ end }}
|
||||
{{ define "content" }}
|
||||
<div id="page-header">
|
||||
<h1 id="welcome" class="text-lg">{{i18n "AuthWithSSBWelcome"}}</h1>
|
||||
</div>
|
||||
<div>
|
||||
<pre>{{.}}</pre>
|
||||
</div>
|
||||
{{end}}
|
|
@ -34,7 +34,7 @@
|
|||
<span class="text-green-500 text-sm">{{$user.Nickname}}</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>
|
||||
|
|
Loading…
Reference in New Issue