various fixes
* fill in sid and sc * fix logout * cascade member removal * fix links * trim suffix from signature * fix /sse/login link * fix sse links and filenames * fix logout * fix typos * fix test compilation * fix bridge test * correct alias url test * add some comments * fix potentiall "can't send" deadlock on bridge some documentation and license headers
This commit is contained in:
parent
36679e5c65
commit
4325e0fb3d
@ -19,8 +19,6 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
|
||||
// debug
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
@ -32,6 +30,7 @@ import (
|
||||
"go.cryptoscope.co/muxrpc/v2/debug"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/sqlite"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
|
||||
mksrv "github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
|
||||
|
@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package signinwithssb
|
||||
|
||||
import (
|
||||
@ -29,8 +31,7 @@ func NewSignalBridge() *SignalBridge {
|
||||
}
|
||||
|
||||
// RegisterSession registers a new session on the bridge.
|
||||
// It returns a channel from which future events can be read
|
||||
// and the server challenge, which acts as the session key.
|
||||
// It returns a fresh server challenge, which acts as the session key.
|
||||
func (sb *SignalBridge) RegisterSession() string {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
@ -38,7 +39,7 @@ func (sb *SignalBridge) RegisterSession() string {
|
||||
c := GenerateChallenge()
|
||||
_, used := sb.sessions[c]
|
||||
if used {
|
||||
for used { // generate new challanges until we have an un-used one
|
||||
for used { // generate new challenges until we have an un-used one
|
||||
c = GenerateChallenge()
|
||||
_, used = sb.sessions[c]
|
||||
}
|
||||
@ -57,6 +58,8 @@ func (sb *SignalBridge) RegisterSession() string {
|
||||
return c
|
||||
}
|
||||
|
||||
// GetEventChannel returns the channel for the passed challenge from which future events can be read.
|
||||
// If sc doesn't exist, the 2nd argument is false.
|
||||
func (sb *SignalBridge) GetEventChannel(sc string) (<-chan Event, bool) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
@ -64,7 +67,7 @@ func (sb *SignalBridge) GetEventChannel(sc string) (<-chan Event, bool) {
|
||||
return ch, has
|
||||
}
|
||||
|
||||
// CompleteSession uses the passed challange to send on and close the open channel.
|
||||
// CompleteSession uses the passed challenge to send on and close the open channel.
|
||||
// It will return an error if the session doesn't exist.
|
||||
func (sb *SignalBridge) CompleteSession(sc string, success bool, token string) error {
|
||||
sb.mu.Lock()
|
||||
@ -75,14 +78,28 @@ func (sb *SignalBridge) CompleteSession(sc string, success bool, token string) e
|
||||
return fmt.Errorf("no such session")
|
||||
}
|
||||
|
||||
ch <- Event{
|
||||
Worked: success,
|
||||
Token: token,
|
||||
}
|
||||
close(ch)
|
||||
var (
|
||||
err error
|
||||
timeout = time.NewTimer(1 * time.Minute)
|
||||
|
||||
// remove session
|
||||
evt = Event{
|
||||
Worked: success,
|
||||
Token: token,
|
||||
}
|
||||
)
|
||||
|
||||
// handle what happens if the sse client isn't connected
|
||||
select {
|
||||
case <-timeout.C:
|
||||
err = fmt.Errorf("faled to send completed session")
|
||||
|
||||
case ch <- evt:
|
||||
timeout.Stop()
|
||||
}
|
||||
|
||||
// session is finalized either way
|
||||
close(ch)
|
||||
delete(sb.sessions, sc)
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package signinwithssb
|
||||
|
||||
import (
|
||||
@ -13,18 +15,21 @@ func TestBridge(t *testing.T) {
|
||||
sb := NewSignalBridge()
|
||||
|
||||
// try to use a non-existant session
|
||||
err := sb.CompleteSession("nope", false)
|
||||
err := sb.CompleteSession("nope", false, "nope-token")
|
||||
a.Error(err)
|
||||
|
||||
// make a new session
|
||||
updates, sc := sb.RegisterSession()
|
||||
sc := sb.RegisterSession()
|
||||
|
||||
b, err := DecodeChallengeString(sc)
|
||||
a.NoError(err)
|
||||
a.Len(b, challengeLength)
|
||||
|
||||
updates, has := sb.GetEventChannel(sc)
|
||||
a.True(has)
|
||||
|
||||
go func() {
|
||||
err := sb.CompleteSession(sc, true)
|
||||
err := sb.CompleteSession(sc, true, "a token")
|
||||
a.NoError(err)
|
||||
}()
|
||||
|
||||
@ -33,6 +38,7 @@ func TestBridge(t *testing.T) {
|
||||
select {
|
||||
case evt := <-updates:
|
||||
a.True(evt.Worked)
|
||||
a.Equal("a token", evt.Token)
|
||||
default:
|
||||
t.Error("no updates")
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package signinwithssb
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package signinwithssb
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package signinwithssb
|
||||
|
||||
import (
|
||||
@ -5,6 +7,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"go.cryptoscope.co/muxrpc/v2"
|
||||
@ -16,7 +19,7 @@ import (
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
)
|
||||
|
||||
// Handler implements the muxrpc methods for alias registration and recvocation
|
||||
// Handler implements the muxrpc methods for the "Sign-in with SSB" calls. SendSolution and InvalidateAllSolutions.
|
||||
type Handler struct {
|
||||
logger kitlog.Logger
|
||||
self refs.FeedRef
|
||||
@ -29,7 +32,7 @@ type Handler struct {
|
||||
roomDomain string // the http(s) domain of the room to signal redirect addresses
|
||||
}
|
||||
|
||||
// New returns a fresh alias muxrpc handler
|
||||
// New returns the muxrpc handler for Sign-in with SSB
|
||||
func New(
|
||||
log kitlog.Logger,
|
||||
self refs.FeedRef,
|
||||
@ -50,6 +53,9 @@ func New(
|
||||
return h
|
||||
}
|
||||
|
||||
// SendSolution implements the receiving end of httpAuth.sendSolution.
|
||||
// It recevies three parameters [sc, cc, sol], does the validation and if it passes creates a token
|
||||
// and signals the created token to the SSE HTTP handler using the signal bridge.
|
||||
func (h Handler) SendSolution(ctx context.Context, req *muxrpc.Request) (interface{}, error) {
|
||||
clientID, err := network.GetFeedRefFromAddr(req.RemoteAddr())
|
||||
if err != nil {
|
||||
@ -76,7 +82,7 @@ func (h Handler) SendSolution(ctx context.Context, req *muxrpc.Request) (interfa
|
||||
sol.ClientID = *clientID
|
||||
sol.ClientChallenge = params[1]
|
||||
|
||||
sig, err := base64.StdEncoding.DecodeString(params[2])
|
||||
sig, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(params[2], ".sig.ed25519"))
|
||||
if err != nil {
|
||||
h.bridge.CompleteSession(sol.ServerChallenge, false, "")
|
||||
return nil, fmt.Errorf("signature is not valid base64 data: %w", err)
|
||||
@ -92,10 +98,16 @@ func (h Handler) SendSolution(ctx context.Context, req *muxrpc.Request) (interfa
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.bridge.CompleteSession(sol.ServerChallenge, true, tok)
|
||||
err = h.bridge.CompleteSession(sol.ServerChallenge, true, tok)
|
||||
if err != nil {
|
||||
h.sessions.RemoveToken(ctx, tok)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// InvalidateAllSolutions implements the muxrpc call httpAuth.invalidateAllSolutions
|
||||
func (h Handler) InvalidateAllSolutions(ctx context.Context, req *muxrpc.Request) (interface{}, error) {
|
||||
// get the feed from the muxrpc connection
|
||||
clientID, err := network.GetFeedRefFromAddr(req.RemoteAddr())
|
||||
@ -103,11 +115,13 @@ func (h Handler) InvalidateAllSolutions(ctx context.Context, req *muxrpc.Request
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// lookup the member
|
||||
member, err := h.members.GetByFeed(ctx, *clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// delete all SIWSSB sessions of that member
|
||||
err = h.sessions.WipeTokensForMember(ctx, member.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -10,6 +10,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.cryptoscope.co/muxrpc/v2"
|
||||
@ -106,7 +108,10 @@ func TestAliasRegister(t *testing.T) {
|
||||
r.NoError(err)
|
||||
t.Log("got URL:", resolveURL)
|
||||
a.Equal("srv", resolveURL.Host)
|
||||
a.Equal("/bob", resolveURL.Path)
|
||||
|
||||
wantURL, err := router.CompleteApp().Get(router.CompleteAliasResolve).URL("alias", "bob")
|
||||
r.NoError(err)
|
||||
a.Equal(wantURL.Path, resolveURL.Path)
|
||||
|
||||
// server should have the alias now
|
||||
alias, err := serv.Aliases.Resolve(ctx, "bob")
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/testutils"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/sqlite"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
|
||||
)
|
||||
@ -88,7 +89,8 @@ func makeNamedTestBot(t testing.TB, name string, opts []roomsrv.Option) (roomdb.
|
||||
t.Log("db close failed: ", err)
|
||||
}
|
||||
})
|
||||
theBot, err := roomsrv.New(db.Members, db.Aliases, name, botOptions...)
|
||||
sb := signinwithssb.NewSignalBridge()
|
||||
theBot, err := roomsrv.New(db.Members, db.Aliases, db.AuthWithSSB, sb, name, botOptions...)
|
||||
r.NoError(err)
|
||||
return db.Members, theBot
|
||||
}
|
||||
|
@ -28,7 +28,9 @@ import (
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/testutils"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
)
|
||||
@ -117,7 +119,11 @@ func (ts *testSession) startGoServer(
|
||||
}),
|
||||
)
|
||||
|
||||
srv, err := roomsrv.New(membersDB, aliasDB, "go.test.room.server", opts...)
|
||||
// not needed for testing yet
|
||||
sb := signinwithssb.NewSignalBridge()
|
||||
authSessionsDB := new(mockdb.FakeAuthWithSSBService)
|
||||
|
||||
srv, err := roomsrv.New(membersDB, aliasDB, authSessionsDB, sb, "go.test.room.server", opts...)
|
||||
r.NoError(err, "failed to init tees a server")
|
||||
ts.t.Logf("go server: %s", srv.Whoami().Ref())
|
||||
ts.t.Cleanup(func() {
|
||||
|
@ -44,6 +44,9 @@ type AuthWithSSBService interface {
|
||||
// CheckToken checks if the passed token is still valid and returns the member id if so
|
||||
CheckToken(ctx context.Context, token string) (int64, error)
|
||||
|
||||
// RemoveToken removes a single token from the database
|
||||
RemoveToken(ctx context.Context, token string) error
|
||||
|
||||
// WipeTokensForMember deletes all tokens currently held for that member
|
||||
WipeTokensForMember(ctx context.Context, memberID int64) error
|
||||
}
|
||||
|
@ -37,6 +37,18 @@ type FakeAuthWithSSBService struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}
|
||||
RemoveTokenStub func(context.Context, string) error
|
||||
removeTokenMutex sync.RWMutex
|
||||
removeTokenArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}
|
||||
removeTokenReturns struct {
|
||||
result1 error
|
||||
}
|
||||
removeTokenReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
WipeTokensForMemberStub func(context.Context, int64) error
|
||||
wipeTokensForMemberMutex sync.RWMutex
|
||||
wipeTokensForMemberArgsForCall []struct {
|
||||
@ -183,6 +195,68 @@ func (fake *FakeAuthWithSSBService) CreateTokenReturnsOnCall(i int, result1 stri
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) RemoveToken(arg1 context.Context, arg2 string) error {
|
||||
fake.removeTokenMutex.Lock()
|
||||
ret, specificReturn := fake.removeTokenReturnsOnCall[len(fake.removeTokenArgsForCall)]
|
||||
fake.removeTokenArgsForCall = append(fake.removeTokenArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}{arg1, arg2})
|
||||
stub := fake.RemoveTokenStub
|
||||
fakeReturns := fake.removeTokenReturns
|
||||
fake.recordInvocation("RemoveToken", []interface{}{arg1, arg2})
|
||||
fake.removeTokenMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) RemoveTokenCallCount() int {
|
||||
fake.removeTokenMutex.RLock()
|
||||
defer fake.removeTokenMutex.RUnlock()
|
||||
return len(fake.removeTokenArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) RemoveTokenCalls(stub func(context.Context, string) error) {
|
||||
fake.removeTokenMutex.Lock()
|
||||
defer fake.removeTokenMutex.Unlock()
|
||||
fake.RemoveTokenStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) RemoveTokenArgsForCall(i int) (context.Context, string) {
|
||||
fake.removeTokenMutex.RLock()
|
||||
defer fake.removeTokenMutex.RUnlock()
|
||||
argsForCall := fake.removeTokenArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) RemoveTokenReturns(result1 error) {
|
||||
fake.removeTokenMutex.Lock()
|
||||
defer fake.removeTokenMutex.Unlock()
|
||||
fake.RemoveTokenStub = nil
|
||||
fake.removeTokenReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) RemoveTokenReturnsOnCall(i int, result1 error) {
|
||||
fake.removeTokenMutex.Lock()
|
||||
defer fake.removeTokenMutex.Unlock()
|
||||
fake.RemoveTokenStub = nil
|
||||
if fake.removeTokenReturnsOnCall == nil {
|
||||
fake.removeTokenReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.removeTokenReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) WipeTokensForMember(arg1 context.Context, arg2 int64) error {
|
||||
fake.wipeTokensForMemberMutex.Lock()
|
||||
ret, specificReturn := fake.wipeTokensForMemberReturnsOnCall[len(fake.wipeTokensForMemberArgsForCall)]
|
||||
@ -252,6 +326,8 @@ func (fake *FakeAuthWithSSBService) Invocations() map[string][][]interface{} {
|
||||
defer fake.checkTokenMutex.RUnlock()
|
||||
fake.createTokenMutex.RLock()
|
||||
defer fake.createTokenMutex.RUnlock()
|
||||
fake.removeTokenMutex.RLock()
|
||||
defer fake.removeTokenMutex.RUnlock()
|
||||
fake.wipeTokensForMemberMutex.RLock()
|
||||
defer fake.wipeTokensForMemberMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
|
@ -114,6 +114,12 @@ func (a AuthWithSSB) CheckToken(ctx context.Context, token string) (int64, error
|
||||
return memberID, nil
|
||||
}
|
||||
|
||||
// RemoveToken removes a single token from the database
|
||||
func (a AuthWithSSB) RemoveToken(ctx context.Context, token string) error {
|
||||
_, err := models.SIWSSBSessions(qm.Where("token = ?", token)).DeleteAll(ctx, a.db)
|
||||
return err
|
||||
}
|
||||
|
||||
// WipeTokensForMember deletes all tokens currently held for that member
|
||||
func (a AuthWithSSB) WipeTokensForMember(ctx context.Context, memberID int64) error {
|
||||
return transact(a.db, func(tx *sql.Tx) error {
|
||||
|
@ -18,7 +18,7 @@ CREATE TABLE fallback_passwords (
|
||||
|
||||
member_id INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY ( member_id ) REFERENCES members( "id" )
|
||||
FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX fallback_passwords_by_login ON fallback_passwords(login);
|
||||
|
||||
@ -32,7 +32,7 @@ CREATE TABLE invites (
|
||||
alias_suggestion TEXT NOT NULL DEFAULT "", -- optional
|
||||
active boolean NOT NULL DEFAULT TRUE,
|
||||
|
||||
FOREIGN KEY ( created_by ) REFERENCES members( "id" )
|
||||
FOREIGN KEY ( created_by ) REFERENCES members( "id" ) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX invite_active_ids ON invites(id) WHERE active=TRUE;
|
||||
CREATE UNIQUE INDEX invite_active_tokens ON invites(hashed_token) WHERE active=TRUE;
|
||||
@ -45,7 +45,7 @@ CREATE TABLE aliases (
|
||||
member_id INTEGER NOT NULL,
|
||||
signature BLOB NOT NULL,
|
||||
|
||||
FOREIGN KEY ( member_id ) REFERENCES members( "id" )
|
||||
FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX aliases_ids ON aliases(id);
|
||||
CREATE UNIQUE INDEX aliases_names ON aliases(name);
|
||||
|
@ -6,10 +6,10 @@ CREATE TABLE SIWSSB_sessions (
|
||||
member_id INTEGER NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY ( member_id ) REFERENCES members( "id" )
|
||||
FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX SIWSSB_by_token ON SIWSSB_sessions(token);
|
||||
CREATE UNIQUE INDEX SIWSSB_by_member ON SIWSSB_sessions(member_id);
|
||||
CREATE INDEX SIWSSB_by_member ON SIWSSB_sessions(member_id);
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE SIWSSB_sessions;
|
||||
|
@ -1,5 +1,5 @@
|
||||
// get the challange from out of the HTML
|
||||
let sc = document.querySelector("#challange").attributes.ch.value
|
||||
// get the challenge from out of the HTML
|
||||
let sc = document.querySelector("#challenge").attributes.ch.value
|
||||
var evtSource = new EventSource(`/sse/events?sc=${sc}`);
|
||||
|
||||
var ping = document.querySelector('#ping');
|
@ -48,7 +48,7 @@ func (a aliasHandler) resolve(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
alias, err := a.db.Resolve(req.Context(), name)
|
||||
if err != nil {
|
||||
ar.SendError(err)
|
||||
ar.SendError(fmt.Errorf("aliases: failed to resolve name %q: %w", name, err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,4 @@ func NewFallbackPasswordHandler(
|
||||
|
||||
// hook up the auth handler to the router
|
||||
m.Get(router.AuthFallbackSignIn).HandlerFunc(ah.Authorize)
|
||||
|
||||
m.Get(router.AuthSignOut).HandlerFunc(ah.Logout)
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
@ -72,7 +73,7 @@ func NewWithSSBHandler(
|
||||
|
||||
m.Get(router.AuthWithSSBSignIn).HandlerFunc(r.HTML("auth/withssb_sign_in.tmpl", ssb.login))
|
||||
|
||||
m.HandleFunc("/sse/login/{sc}", r.HTML("auth/withssb_server_start.tmpl", ssb.startWithServer))
|
||||
m.HandleFunc("/sse/login", r.HTML("auth/withssb_server_start.tmpl", ssb.startWithServer))
|
||||
m.HandleFunc("/sse/events", ssb.eventSource)
|
||||
|
||||
return &ssb
|
||||
@ -86,9 +87,6 @@ func (h WithSSBHandler) login(w http.ResponseWriter, req *http.Request) (interfa
|
||||
|
||||
// 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
|
||||
@ -110,10 +108,11 @@ func (h WithSSBHandler) login(w http.ResponseWriter, req *http.Request) (interfa
|
||||
// check that we have that member
|
||||
member, err := h.membersdb.GetByFeed(req.Context(), client)
|
||||
if err != nil {
|
||||
errMsg := fmt.Errorf("sign-in with ssb: client isnt a member: %w", err)
|
||||
if err == roomdb.ErrNotFound {
|
||||
return nil, weberrors.ErrForbidden{Details: fmt.Errorf("sign-in: client isnt a member")}
|
||||
return nil, weberrors.ErrForbidden{Details: errMsg}
|
||||
}
|
||||
return nil, err
|
||||
return nil, errMsg
|
||||
}
|
||||
clientReq.ClientID = client
|
||||
|
||||
@ -134,13 +133,14 @@ func (h WithSSBHandler) login(w http.ResponseWriter, req *http.Request) (interfa
|
||||
var solution string
|
||||
err = edp.Async(ctx, &solution, muxrpc.TypeString, muxrpc.Method{"httpAuth", "requestSolution"}, sc, cc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("sign-in with ssb: could not request solution from client: %w", err)
|
||||
}
|
||||
|
||||
// decode and validate the response
|
||||
solutionBytes, err := base64.URLEncoding.DecodeString(solution)
|
||||
solution = strings.TrimSuffix(solution, ".sig.ed25519")
|
||||
solutionBytes, err := base64.StdEncoding.DecodeString(solution)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("sign-in with ssb: failed to decode solution: %w", err)
|
||||
}
|
||||
|
||||
if !clientReq.Validate(solutionBytes) {
|
||||
@ -150,17 +150,20 @@ func (h WithSSBHandler) login(w http.ResponseWriter, req *http.Request) (interfa
|
||||
// create a session for invalidation
|
||||
tok, err := h.sessiondb.CreateToken(req.Context(), member.ID)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("sign-in with ssb: could not create token: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, err := h.cookieStore.Get(req, siwssbSessionName)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("sign-in with ssb: failed to load cookie session: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session.Values[memberToken] = tok
|
||||
session.Values[userTimeout] = time.Now().Add(lifetime)
|
||||
if err := session.Save(req, w); err != nil {
|
||||
err = fmt.Errorf("sign-in with ssb: failed to update cookie session: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -185,19 +188,6 @@ const (
|
||||
|
||||
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.
|
||||
@ -249,26 +239,35 @@ func (h WithSSBHandler) AuthenticateRequest(r *http.Request) (*roomdb.Member, er
|
||||
}
|
||||
|
||||
// Logout destroys the session data and updates the cookie with an invalidated one.
|
||||
func (h WithSSBHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
func (h WithSSBHandler) Logout(w http.ResponseWriter, r *http.Request) error {
|
||||
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
|
||||
return err
|
||||
}
|
||||
|
||||
tokenVal, ok := session.Values[memberToken]
|
||||
if !ok {
|
||||
// not a sign-in with ssb session
|
||||
return nil
|
||||
}
|
||||
|
||||
token, ok := tokenVal.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("wrong token type: %T", tokenVal)
|
||||
}
|
||||
|
||||
err = h.sessiondb.RemoveToken(r.Context(), token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.Values[userTimeout] = time.Now().Add(-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
|
||||
return err
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// server-sent-events stuff
|
||||
@ -278,24 +277,26 @@ func (h WithSSBHandler) startWithServer(w http.ResponseWriter, req *http.Request
|
||||
|
||||
var queryParams = make(url.Values)
|
||||
queryParams.Set("action", "start-http-auth")
|
||||
queryParams.Set("sid", h.roomID.Ref())
|
||||
queryParams.Set("sc", sc)
|
||||
|
||||
var startAuthURI url.URL
|
||||
startAuthURI.Scheme = "ssb"
|
||||
startAuthURI.Opaque = "experimental"
|
||||
startAuthURI.RawQuery = queryParams.Encode()
|
||||
|
||||
qrCode, err := qrcode.New(startAuthURI.String(), qrcode.High)
|
||||
// generate a QR code with the token inside so that you can open it easily in a supporting mobile app
|
||||
qrCode, err := qrcode.New(startAuthURI.String(), qrcode.Medium)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
qrCode.BackgroundColor = color.RGBA{R: 0xf9, G: 0xfa, B: 0xfb}
|
||||
qrCode.ForegroundColor = color.Black
|
||||
|
||||
qrCodeData, err := qrCode.PNG(-8)
|
||||
qrCodeData, err := qrCode.PNG(-5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qrURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(qrCodeData)
|
||||
|
||||
return struct {
|
||||
@ -383,7 +384,7 @@ func (h WithSSBHandler) eventSource(w http.ResponseWriter, r *http.Request) {
|
||||
case update := <-evtCh:
|
||||
evt := event{
|
||||
ID: evtID,
|
||||
Data: "challange validation failed",
|
||||
Data: "challenge validation failed",
|
||||
Event: "failed",
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
@ -193,9 +195,9 @@ func TestAuthWithSSBNotConnected(t *testing.T) {
|
||||
|
||||
urlTo := web.NewURLTo(ts.Router)
|
||||
|
||||
signInStartURL := urlTo(router.AuthWithSSBSignIn,
|
||||
signInStartURL := urlTo(router.AuthLogin,
|
||||
"cid", client.Feed.Ref(),
|
||||
"challenge", cc,
|
||||
"cc", cc,
|
||||
)
|
||||
r.NotNil(signInStartURL)
|
||||
|
||||
@ -224,9 +226,9 @@ func TestAuthWithSSBNotAllowed(t *testing.T) {
|
||||
|
||||
urlTo := web.NewURLTo(ts.Router)
|
||||
|
||||
signInStartURL := urlTo(router.AuthWithSSBSignIn,
|
||||
signInStartURL := urlTo(router.AuthLogin,
|
||||
"cid", client.Feed.Ref(),
|
||||
"challenge", cc,
|
||||
"cc", cc,
|
||||
)
|
||||
r.NotNil(signInStartURL)
|
||||
|
||||
@ -293,7 +295,7 @@ func TestAuthWithSSBHasClient(t *testing.T) {
|
||||
// sign the request now that we have the sc
|
||||
clientSig := req.Sign(client.Pair.Secret)
|
||||
|
||||
*strptr = base64.URLEncoding.EncodeToString(clientSig)
|
||||
*strptr = base64.StdEncoding.EncodeToString(clientSig)
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -305,7 +307,8 @@ func TestAuthWithSSBHasClient(t *testing.T) {
|
||||
req.ClientChallenge = cc
|
||||
|
||||
// prepare the url
|
||||
signInStartURL := web.NewURLTo(ts.Router)(router.AuthWithSSBSignIn,
|
||||
urlTo := web.NewURLTo(ts.Router)
|
||||
signInStartURL := urlTo(router.AuthLogin,
|
||||
"cid", client.Feed.Ref(),
|
||||
"cc", cc,
|
||||
)
|
||||
@ -316,7 +319,11 @@ func TestAuthWithSSBHasClient(t *testing.T) {
|
||||
|
||||
t.Log(signInStartURL.String())
|
||||
doc, resp := ts.Client.GetHTML(signInStartURL.String())
|
||||
a.Equal(http.StatusOK, resp.Code)
|
||||
a.Equal(http.StatusTemporaryRedirect, resp.Code)
|
||||
|
||||
dashboardURL, err := ts.Router.Get(router.AdminDashboard).URL()
|
||||
r.Nil(err)
|
||||
a.Equal(dashboardURL.Path, resp.Header().Get("Location"))
|
||||
|
||||
webassert.Localized(t, doc, []webassert.LocalizedElement{
|
||||
// {"#welcome", "AuthWithSSBWelcome"},
|
||||
@ -337,8 +344,7 @@ func TestAuthWithSSBHasClient(t *testing.T) {
|
||||
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"
|
||||
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
@ -245,6 +246,14 @@ func New(
|
||||
// just hooks up the router to the handler
|
||||
roomsAuth.NewFallbackPasswordHandler(m, r, authWithPassword)
|
||||
|
||||
m.Get(router.AuthSignOut).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
err = authWithSSB.Logout(w, req)
|
||||
if err != nil {
|
||||
level.Warn(logging.FromContext(req.Context())).Log("err", err)
|
||||
}
|
||||
authWithPassword.Logout(w, req)
|
||||
})
|
||||
|
||||
adminHandler := admin.Handler(
|
||||
netInfo.Domain,
|
||||
r,
|
||||
|
@ -16,15 +16,16 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"go.mindeco.de/http/tester"
|
||||
"go.mindeco.de/logging/logtest"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network/mocked"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
)
|
||||
|
||||
type testSession struct {
|
||||
@ -46,6 +47,8 @@ type testSession struct {
|
||||
|
||||
MockedEndpoints *mocked.FakeEndpoints
|
||||
|
||||
SignalBridge *signinwithssb.SignalBridge
|
||||
|
||||
NetworkInfo NetworkInfo
|
||||
}
|
||||
|
||||
@ -98,12 +101,15 @@ func setup(t *testing.T) *testSession {
|
||||
|
||||
ts.Router = router.CompleteApp()
|
||||
|
||||
ts.SignalBridge = signinwithssb.NewSignalBridge()
|
||||
|
||||
h, err := New(
|
||||
log,
|
||||
testRepo,
|
||||
ts.NetworkInfo,
|
||||
ts.RoomState,
|
||||
ts.MockedEndpoints,
|
||||
ts.SignalBridge,
|
||||
Databases{
|
||||
Aliases: ts.AliasesDB,
|
||||
AuthFallback: ts.AuthFallbackDB,
|
||||
|
@ -4,6 +4,7 @@ GenericSave = "Save"
|
||||
GenericCreate = "Create"
|
||||
GenericPreview = "Preview"
|
||||
GenericLanguage = "Language"
|
||||
GenericOpenLink = "Open Link"
|
||||
|
||||
PageNotFound = "The requested page was not found."
|
||||
|
||||
@ -20,7 +21,7 @@ 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."
|
||||
AuthWithSSBWelcome = "If you have a compatible device/application, you can sign-in here without a password. Open the QR-Code on your mobile device to complete the process or click the link below."
|
||||
|
||||
AdminDashboardWelcome = "Welcome to your dashboard"
|
||||
AdminDashboardTitle = "Room Admin Dashboard"
|
||||
|
@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package members implements helpers for accessing the currently logged in admin or moderator of an active request.
|
||||
package members
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package members
|
||||
|
||||
import (
|
||||
|
@ -9,23 +9,23 @@ const (
|
||||
AuthFallbackSignInForm = "auth:fallback:signin:form"
|
||||
AuthFallbackSignIn = "auth:fallback:signin"
|
||||
|
||||
AuthWithSSBSignIn = "auth:ssb:signin"
|
||||
AuthWithSSBSignIn = "auth:ssb:login"
|
||||
// AuthWithSSBSignIn
|
||||
|
||||
AuthSignOut = "auth:logout"
|
||||
)
|
||||
|
||||
// NewSignin constructs a mux.Router containing the routes for sign-in and -out
|
||||
// Auth constructs a mux.Router containing the routes for sign-in and -out
|
||||
func Auth(m *mux.Router) *mux.Router {
|
||||
if m == nil {
|
||||
m = mux.NewRouter()
|
||||
}
|
||||
|
||||
// register fallback
|
||||
m.Path("/fallback/signin").Methods("GET").Name(AuthFallbackSignInForm)
|
||||
m.Path("/fallback/signin").Methods("POST").Name(AuthFallbackSignIn)
|
||||
|
||||
m.Path("/withssb/signin").Methods("GET").Name(AuthWithSSBSignIn)
|
||||
// register password fallback
|
||||
m.Path("/password/signin").Methods("GET").Name(AuthFallbackSignInForm)
|
||||
m.Path("/password/signin").Methods("POST").Name(AuthFallbackSignIn)
|
||||
|
||||
m.Path("/login").Methods("GET").Name(AuthWithSSBSignIn)
|
||||
m.Path("/logout").Methods("GET").Name(AuthSignOut)
|
||||
|
||||
return m
|
||||
|
@ -24,13 +24,13 @@ const (
|
||||
func CompleteApp() *mux.Router {
|
||||
m := mux.NewRouter()
|
||||
|
||||
Auth(m.PathPrefix("/auth").Subrouter())
|
||||
Auth(m)
|
||||
Admin(m.PathPrefix("/admin").Subrouter())
|
||||
|
||||
m.Path("/").Methods("GET").Name(CompleteIndex)
|
||||
m.Path("/about").Methods("GET").Name(CompleteAbout)
|
||||
|
||||
m.Path("/{alias}").Methods("GET").Name(CompleteAliasResolve)
|
||||
m.Path("/alias/{alias}").Methods("GET").Name(CompleteAliasResolve)
|
||||
|
||||
m.Path("/invite/accept").Methods("GET").Name(CompleteInviteAccept)
|
||||
m.Path("/invite/consume").Methods("POST").Name(CompleteInviteConsume)
|
||||
|
@ -1,11 +1,11 @@
|
||||
{{ define "title" }}{{i18n "AdminAllowListRemoveConfirmTitle"}}{{ end }}
|
||||
{{ define "title" }}{{i18n "AdminMembersRemoveConfirmTitle"}}{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="flex flex-col justify-center items-center h-64">
|
||||
|
||||
<span
|
||||
id="welcome"
|
||||
class="text-center"
|
||||
>{{i18n "AdminAllowListRemoveConfirmWelcome"}}</span>
|
||||
>{{i18n "AdminMembersRemoveConfirmWelcome"}}</span>
|
||||
|
||||
<pre
|
||||
id="verify"
|
||||
|
@ -4,14 +4,13 @@
|
||||
<h1 id="welcome" class="text-lg">{{i18n "AuthWithSSBWelcome"}}</h1>
|
||||
</div>
|
||||
<div>
|
||||
<h3>TODO: qr code of the code</h3>
|
||||
<img src="{{.QRCodeURI}}" alt="QR-Code to pass challange to an App" />
|
||||
<pre>{{.SSBURI}}</pre>
|
||||
<img src="{{.QRCodeURI}}" alt="QR-Code to pass the challenge to an App" />
|
||||
<a href="{{.SSBURI}}">{{i18n "GenericOpenLink"}}</a>
|
||||
|
||||
<h3>Server events</h3>
|
||||
<p id="ping"></p>
|
||||
<p id="failed" class="text-red-500"></p>
|
||||
</div>
|
||||
<div id="challange" ch="{{.ServerChallenge}}"></div>
|
||||
<script src="/assets/events-demo.js"></script>
|
||||
<div id="challenge" ch="{{.ServerChallenge}}"></div>
|
||||
<script src="/assets/login-events.js"></script>
|
||||
{{end}}
|
@ -1,9 +1,6 @@
|
||||
{{ define "title" }}{{i18n "AuthWithSSBTitle"}}{{ end }}
|
||||
{{ define "content" }}
|
||||
<div id="page-header">
|
||||
<h1 id="welcome" class="text-lg">{{i18n "AuthWithSSBWelcome"}}</h1>
|
||||
</div>
|
||||
<div>
|
||||
<pre>{{.}}</pre>
|
||||
<a href="{{urlTo "admin:dashboard"}}">Proceed to Dashboard</a>
|
||||
</div>
|
||||
{{end}}
|
Loading…
Reference in New Issue
Block a user