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:
Henry 2021-03-24 18:31:37 +01:00
parent 36679e5c65
commit 4325e0fb3d
29 changed files with 260 additions and 101 deletions

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
// SPDX-License-Identifier: MIT
package signinwithssb
import (

View File

@ -1,3 +1,5 @@
// SPDX-License-Identifier: MIT
package signinwithssb
import (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
// SPDX-License-Identifier: MIT
// Package members implements helpers for accessing the currently logged in admin or moderator of an active request.
package members

View File

@ -1,3 +1,5 @@
// SPDX-License-Identifier: MIT
package members
import (

View File

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

View File

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

View File

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

View File

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

View File

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