begin sign-in with ssb

* sketch session store
* use session store and unify authentication handling
* sketch muxrpc handlers
This commit is contained in:
Henry 2021-03-17 10:46:05 +01:00
parent b5e2ea6572
commit a180c74c38
36 changed files with 2537 additions and 219 deletions

View File

@ -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
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,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
View File

@ -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=

View File

@ -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

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,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)
}

View File

@ -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))
}

View File

@ -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, &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 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
}

View File

@ -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 {

View File

@ -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

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,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
})
}

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

@ -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

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

@ -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))
}
}

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

@ -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)
}

View File

@ -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
}

View File

@ -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"},
})
}

View File

@ -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,
}

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

@ -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,

View File

@ -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"

View File

@ -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
}

View File

@ -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))
})
}

View File

@ -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
}

View File

@ -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}}

View File

@ -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>