Add initial feed structure, fix passkey id usage

This commit is contained in:
Melody Becker 2025-04-02 13:44:33 +02:00
parent ef91558600
commit 420f6e46c0
Signed by: mstar
SSH key fingerprint: SHA256:9VAo09aaVNTWKzPW7Hq2LW+ox9OdwmTSHRoD4mlz1yI
6 changed files with 162 additions and 118 deletions

View file

@ -3,6 +3,7 @@ package auth
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strings"
"time" "time"
"git.mstar.dev/mstar/goutils/other" "git.mstar.dev/mstar/goutils/other"
@ -26,38 +27,40 @@ import (
// Returns the credential options the passkey needs to sign // Returns the credential options the passkey needs to sign
func (a *Authenticator) StartPasskeyLogin( func (a *Authenticator) StartPasskeyLogin(
username string, username string,
) (*protocol.CredentialAssertion, uint64, error) { ) (*protocol.CredentialAssertion, string, error) {
if ok, err := a.canUsernameLogin(username); !ok { if ok, err := a.canUsernameLogin(username); !ok {
return nil, 0, other.Error("auth", "user may not login", err) return nil, "", other.Error("auth", "user may not login", err)
} }
acc, err := dbgen.User.Where(dbgen.User.Username.Eq(username)).First() acc, err := dbgen.User.Where(dbgen.User.Username.Eq(username)).First()
if err != nil { if err != nil {
return nil, 0, other.Error("auth", "failed to acquire user for login", err) return nil, "", other.Error("auth", "failed to acquire user for login", err)
} }
wrappedAcc := fakeUser{acc} wrappedAcc := fakeUser{acc}
options, session, err := a.webauthn.BeginLogin(&wrappedAcc) options, session, err := a.webauthn.BeginLogin(&wrappedAcc)
if err != nil { if err != nil {
return nil, 0, other.Error("auth", "failed to initiate passkey login", err) return nil, "", other.Error("auth", "failed to initiate passkey login", err)
} }
pkeySession := models.LoginProcessToken{ pkeySession := models.LoginProcessToken{
User: *acc, User: *acc,
UserId: acc.ID, UserId: acc.ID,
ExpiresAt: time.Now().Add(time.Minute * 3), ExpiresAt: time.Now().Add(time.Minute * 3),
Token: string(other.Must(json.Marshal(session))), // Abuse name for session storage since token must be a uuid
Name: "", Name: string(other.Must(json.Marshal(session))),
} }
err = dbgen.LoginProcessToken.Clauses(clause.OnConflict{UpdateAll: true}).Create(&pkeySession) err = dbgen.LoginProcessToken.Clauses(clause.OnConflict{UpdateAll: true}).
Omit(dbgen.LoginProcessToken.Token).
Create(&pkeySession)
if err != nil { if err != nil {
return nil, 0, other.Error("auth", "failed to create login process token", err) return nil, "", other.Error("auth", "failed to create login process token", err)
} }
return options, pkeySession.ID, nil return options, pkeySession.Token, nil
} }
// Complete a passkey login request // Complete a passkey login request
// Takes the username logging in as well as the raw request containing the passkey response // Takes the username logging in as well as the raw request containing the passkey response
func (a *Authenticator) CompletePasskeyLogin( func (a *Authenticator) CompletePasskeyLogin(
username string, username string,
sessionId uint64, sessionId string,
response *http.Request, response *http.Request,
) (accessToken string, err error) { ) (accessToken string, err error) {
// Get user in question // Get user in question
@ -66,7 +69,7 @@ func (a *Authenticator) CompletePasskeyLogin(
return "", other.Error("auth", "failed to get user for passkey login completion", err) return "", other.Error("auth", "failed to get user for passkey login completion", err)
} }
// Get latest login token data // Get latest login token data
loginToken, err := dbgen.LoginProcessToken.Where(dbgen.LoginProcessToken.ID.Eq(sessionId)). loginToken, err := dbgen.LoginProcessToken.Where(dbgen.LoginProcessToken.Token.Eq(sessionId)).
First() First()
if err != nil { if err != nil {
return "", other.Error( return "", other.Error(
@ -80,7 +83,7 @@ func (a *Authenticator) CompletePasskeyLogin(
return "", ErrProcessTimeout return "", ErrProcessTimeout
} }
var pkeySession webauthn.SessionData var pkeySession webauthn.SessionData
err = json.Unmarshal([]byte(loginToken.Token), &pkeySession) err = json.Unmarshal([]byte(loginToken.Name), &pkeySession)
if err != nil { if err != nil {
return "", other.Error("auth", "failed to unmarshal passkey session for user", err) return "", other.Error("auth", "failed to unmarshal passkey session for user", err)
} }
@ -89,6 +92,9 @@ func (a *Authenticator) CompletePasskeyLogin(
if err != nil { if err != nil {
return "", other.Error("auth", "passkey completion failed", err) return "", other.Error("auth", "passkey completion failed", err)
} }
// TODO: Utilise clone warning
// newSession.Authenticator.CloneWarning
jsonSessionId, err := json.Marshal(newSession.ID) jsonSessionId, err := json.Marshal(newSession.ID)
if err != nil { if err != nil {
return "", other.Error("auth", "failed to marshal session", err) return "", other.Error("auth", "failed to marshal session", err)
@ -129,98 +135,84 @@ func (a *Authenticator) CompletePasskeyLogin(
// Start the process of registrating a passkey to an account // Start the process of registrating a passkey to an account
func (a *Authenticator) StartPasskeyRegistration( func (a *Authenticator) StartPasskeyRegistration(
username string, username string,
) (*protocol.CredentialAssertion, error) { passkeyName string,
// p.l.Infof("begin registration") ) (*protocol.CredentialCreation, string, error) {
// if ok, err := a.canUsernameLogin(username); !ok {
// // can we actually do not use the username at all? return nil, "", other.Error("auth", "user may not login", err)
// username, err := getUsername(r) }
// acc, err := dbgen.User.Where(dbgen.User.Username.Eq(username)).First()
// if err != nil { if err != nil {
// p.l.Errorf("can't get username: %s", err.Error()) return nil, "", other.Error("auth", "failed to acquire user for login", err)
// JSONResponse(w, fmt.Sprintf("can't get username: %s", err.Error()), http.StatusBadRequest) }
// wrappedAcc := fakeUser{acc}
// return options, session, err := a.webauthn.BeginRegistration(&wrappedAcc)
// } jsonSession, err := json.Marshal(session)
// if err != nil {
// user := p.userStore.GetOrCreateUser(username) return nil, "", other.Error("auth", "failed to marshal session to json", err)
// }
// options, session, err := p.webAuthn.BeginRegistration(user) pkeySession := models.LoginProcessToken{
// User: *acc,
// if err != nil { UserId: acc.ID,
// msg := fmt.Sprintf("can't begin registration: %s", err.Error()) ExpiresAt: time.Now().Add(time.Minute * 3),
// p.l.Errorf(msg) // Abuse name for storing session and passkey name since token must be a uuid
// JSONResponse(w, msg, http.StatusBadRequest) Name: passkeyName + "---" + string(jsonSession),
// }
// return err = dbgen.LoginProcessToken.Clauses(clause.OnConflict{UpdateAll: true}).
// } Omit(dbgen.LoginProcessToken.Token).
// Create(&pkeySession)
// // Make a session key and store the sessionData values if err != nil {
// t, err := p.sessionStore.GenSessionID() return nil, "", other.Error("auth", "failed to create login process token", err)
// }
// if err != nil { return options, pkeySession.Token, nil
// p.l.Errorf("can't generate session id: %s", err.Error())
// JSONResponse(
// w,
// fmt.Sprintf("can't generate session id: %s", err.Error()),
// http.StatusInternalServerError,
// )
//
// return
// }
//
// p.sessionStore.SaveSession(t, session)
// p.setSessionCookie(w, t)
//
// // return the options generated with the session key
// // options.publicKey contain our registration options
// JSONResponse(w, options, http.StatusOK)
panic("Not implemented") // TODO: Implement me
} }
func (a *Authenticator) CompletePasskeyRegistration(username string) error { func (a *Authenticator) CompletePasskeyRegistration(
// // Get the session key from cookie username string,
// sid, err := r.Cookie(p.cookieSettings.Name) sessionId string,
// response *http.Request,
// if err != nil { ) error {
// p.l.Errorf("can't get session id: %s", err.Error()) // Get latest login token data
// JSONResponse(w, fmt.Sprintf("can't get session id: %s", err.Error()), http.StatusBadRequest) loginToken, err := dbgen.LoginProcessToken.Where(dbgen.LoginProcessToken.Token.Eq(sessionId)).
// First()
// return if err != nil {
// } return other.Error(
// "auth",
// // Get the session data stored from the function above "failed to get user's login token for passkey login completion",
// session, ok := p.sessionStore.GetSession(sid.Value) err,
// )
// if !ok { }
// p.l.Errorf("can't get session data") // Check if that token has expired
// JSONResponse(w, "can't get session data", http.StatusBadRequest) if loginToken.ExpiresAt.Before(time.Now()) {
// return ErrProcessTimeout
// return }
// } var pkeySession webauthn.SessionData
// passkeyName, jsonSession, found := strings.Cut(loginToken.Name, "---")
// user := p.userStore.GetUserByWebAuthnId(session.UserID) // Get the user if !found {
// return ErrInvalidPasskeyRegistrationData
// credential, err := p.webAuthn.FinishRegistration(user, *session, r) }
// err = json.Unmarshal([]byte(jsonSession), &pkeySession)
// if err != nil { if err != nil {
// msg := fmt.Sprintf("can't finish registration: %s", err.Error()) return other.Error("auth", "failed to unmarshal passkey session for user", err)
// p.l.Errorf(msg) }
// wrappedAcc := fakeUser{&loginToken.User}
// p.deleteSessionCookie(w) credential, err := a.webauthn.FinishRegistration(&wrappedAcc, pkeySession, response)
// JSONResponse(w, msg, http.StatusBadRequest) if err != nil {
// return other.Error("auth", "failed to complete passkey registration", err)
// return }
// } jsonCredential, err := json.Marshal(credential)
// if err != nil {
// // If creation was successful, store the credential object return other.Error("auth", "failed to marshal credential to json", err)
// user.PutCredential(*credential) }
// p.userStore.SaveUser(user) authData := models.UserAuthMethod{
// User: loginToken.User,
// p.sessionStore.DeleteSession(sid.Value) UserId: loginToken.UserId,
// p.deleteSessionCookie(w) AuthMethod: models.AuthMethodPasskey,
// Name: passkeyName,
// p.l.Infof("finish registration") Token: jsonCredential,
// JSONResponse(w, "Registration Success", http.StatusOK) }
panic("Not implemented") // TODO: Implement me err = dbgen.UserAuthMethod.Create(&authData)
if err != nil {
return other.Error("auth", "failed to insert new auth method into db", err)
}
return nil
} }

View file

@ -1,11 +1,14 @@
package models package models
// Just a list of all models stored in the database
var AllTypes = []any{ var AllTypes = []any{
&Emote{}, &Emote{},
&Feed{},
&MediaMetadata{}, &MediaMetadata{},
&Note{}, &Note{},
&NoteToAttachment{}, &NoteToAttachment{},
&NoteToEmote{}, &NoteToEmote{},
&NoteToFeed{},
&NoteToPing{}, &NoteToPing{},
&NoteTag{}, &NoteTag{},
&Reaction{}, &Reaction{},

View file

@ -0,0 +1,32 @@
package models
import (
"database/sql"
"gorm.io/gorm"
)
// A feed is the initial entry point for all inbound Activitypub events.
// However, its primary and only user-facing use case is to be a collection
// of inbound messages, nothing else.
//
// Thus, the flow for inbound events is the following:
// If the event is a note:
//
// Add it to the receiving feed. If it's a reply and the feed is a default
// create a notification for the owner
//
// If it's an event:
//
// If the feed is not a default feed for a user, discard the event
// If it is the default feed for a user, create a notification for the owner
type Feed struct {
gorm.Model
Name string
Owner User
OwnerId string
IsDefault bool // Whether the feed is the default one for the user
// If a feed is the default one for a user, use that user's public key.
// Otherwise, use its own key
PublicKey sql.NullString
}

View file

@ -1,5 +0,0 @@
package models
// TODO: Struct for mapping a note to a user for their personal feed
// Storing timeline info in redis could also be an idea, but I kinda like
// everything being in one place

View file

@ -0,0 +1,29 @@
package models
import "time"
// TODO: Struct for mapping a note to a user for their personal feed
// Storing timeline info in redis could also be an idea, but I kinda like
// everything being in one place
// Data needed:
// - Which note
// - Who's feed
// - Which feed (once separate feeds are implemented)
// - Reason:
// - Boost
// - Follow person
// - Follow tag
//
// Also need to store the boosts a user has performed somewhere
// Maybe adjust Reaction? Though a separate table might be a better option
// Assigns a note to a feed
type NoteToFeed struct {
ID uint64 `gorm:"primarykey"`
CreatedAt time.Time
Note Note
NoteId string
// Feed Feed
// FeedId uint64
// Reason AppearanceReason
}

View file

@ -10,15 +10,8 @@ import (
// A user describes an account for creating content. // A user describes an account for creating content.
// This may be controlled by either a human or some external script // This may be controlled by either a human or some external script
// //
// Data stored in external types: // Data stored externally:
// - Custom info fields // - Feed connections (which note belongs in the feed of this user, for what reason)
// - Being types
// - Tags
// - Relations
// - Pronouns
// - Roles
// - AP remote links
// - Auth methods and tokens (hashed pw, totp key, passkey id)
type User struct { type User struct {
// ID is a uuid for this account // ID is a uuid for this account
// //