diff --git a/auth-new/passkey.go b/auth-new/passkey.go index e4423a3..397d76c 100644 --- a/auth-new/passkey.go +++ b/auth-new/passkey.go @@ -3,6 +3,7 @@ package auth import ( "encoding/json" "net/http" + "strings" "time" "git.mstar.dev/mstar/goutils/other" @@ -26,38 +27,40 @@ import ( // Returns the credential options the passkey needs to sign func (a *Authenticator) StartPasskeyLogin( username string, -) (*protocol.CredentialAssertion, uint64, error) { +) (*protocol.CredentialAssertion, string, error) { 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() 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} options, session, err := a.webauthn.BeginLogin(&wrappedAcc) 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{ User: *acc, UserId: acc.ID, ExpiresAt: time.Now().Add(time.Minute * 3), - Token: string(other.Must(json.Marshal(session))), - Name: "", + // Abuse name for session storage since token must be a uuid + 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 { - 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 // Takes the username logging in as well as the raw request containing the passkey response func (a *Authenticator) CompletePasskeyLogin( username string, - sessionId uint64, + sessionId string, response *http.Request, ) (accessToken string, err error) { // 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) } // 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() if err != nil { return "", other.Error( @@ -80,7 +83,7 @@ func (a *Authenticator) CompletePasskeyLogin( return "", ErrProcessTimeout } var pkeySession webauthn.SessionData - err = json.Unmarshal([]byte(loginToken.Token), &pkeySession) + err = json.Unmarshal([]byte(loginToken.Name), &pkeySession) if err != nil { return "", other.Error("auth", "failed to unmarshal passkey session for user", err) } @@ -89,6 +92,9 @@ func (a *Authenticator) CompletePasskeyLogin( if err != nil { return "", other.Error("auth", "passkey completion failed", err) } + // TODO: Utilise clone warning + // newSession.Authenticator.CloneWarning + jsonSessionId, err := json.Marshal(newSession.ID) if err != nil { 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 func (a *Authenticator) StartPasskeyRegistration( username string, -) (*protocol.CredentialAssertion, error) { - // p.l.Infof("begin registration") - // - // // can we actually do not use the username at all? - // username, err := getUsername(r) - // - // if err != nil { - // p.l.Errorf("can't get username: %s", err.Error()) - // JSONResponse(w, fmt.Sprintf("can't get username: %s", err.Error()), http.StatusBadRequest) - // - // return - // } - // - // user := p.userStore.GetOrCreateUser(username) - // - // options, session, err := p.webAuthn.BeginRegistration(user) - // - // if err != nil { - // msg := fmt.Sprintf("can't begin registration: %s", err.Error()) - // p.l.Errorf(msg) - // JSONResponse(w, msg, http.StatusBadRequest) - // - // return - // } - // - // // Make a session key and store the sessionData values - // t, err := p.sessionStore.GenSessionID() - // - // if err != 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 + passkeyName string, +) (*protocol.CredentialCreation, string, error) { + if ok, err := a.canUsernameLogin(username); !ok { + return nil, "", other.Error("auth", "user may not login", err) + } + acc, err := dbgen.User.Where(dbgen.User.Username.Eq(username)).First() + if err != nil { + return nil, "", other.Error("auth", "failed to acquire user for login", err) + } + wrappedAcc := fakeUser{acc} + options, session, err := a.webauthn.BeginRegistration(&wrappedAcc) + jsonSession, err := json.Marshal(session) + if err != nil { + return nil, "", other.Error("auth", "failed to marshal session to json", err) + } + pkeySession := models.LoginProcessToken{ + User: *acc, + UserId: acc.ID, + ExpiresAt: time.Now().Add(time.Minute * 3), + // Abuse name for storing session and passkey name since token must be a uuid + Name: passkeyName + "---" + string(jsonSession), + } + err = dbgen.LoginProcessToken.Clauses(clause.OnConflict{UpdateAll: true}). + Omit(dbgen.LoginProcessToken.Token). + Create(&pkeySession) + if err != nil { + return nil, "", other.Error("auth", "failed to create login process token", err) + } + return options, pkeySession.Token, nil } -func (a *Authenticator) CompletePasskeyRegistration(username string) error { - // // Get the session key from cookie - // sid, err := r.Cookie(p.cookieSettings.Name) - // - // if err != nil { - // p.l.Errorf("can't get session id: %s", err.Error()) - // JSONResponse(w, fmt.Sprintf("can't get session id: %s", err.Error()), http.StatusBadRequest) - // - // return - // } - // - // // Get the session data stored from the function above - // session, ok := p.sessionStore.GetSession(sid.Value) - // - // if !ok { - // p.l.Errorf("can't get session data") - // JSONResponse(w, "can't get session data", http.StatusBadRequest) - // - // return - // } - // - // user := p.userStore.GetUserByWebAuthnId(session.UserID) // Get the user - // - // credential, err := p.webAuthn.FinishRegistration(user, *session, r) - // - // if err != nil { - // msg := fmt.Sprintf("can't finish registration: %s", err.Error()) - // p.l.Errorf(msg) - // - // p.deleteSessionCookie(w) - // JSONResponse(w, msg, http.StatusBadRequest) - // - // return - // } - // - // // If creation was successful, store the credential object - // user.PutCredential(*credential) - // p.userStore.SaveUser(user) - // - // p.sessionStore.DeleteSession(sid.Value) - // p.deleteSessionCookie(w) - // - // p.l.Infof("finish registration") - // JSONResponse(w, "Registration Success", http.StatusOK) - panic("Not implemented") // TODO: Implement me +func (a *Authenticator) CompletePasskeyRegistration( + username string, + sessionId string, + response *http.Request, +) error { + // Get latest login token data + loginToken, err := dbgen.LoginProcessToken.Where(dbgen.LoginProcessToken.Token.Eq(sessionId)). + First() + if err != nil { + return other.Error( + "auth", + "failed to get user's login token for passkey login completion", + err, + ) + } + // Check if that token has expired + if loginToken.ExpiresAt.Before(time.Now()) { + return ErrProcessTimeout + } + var pkeySession webauthn.SessionData + passkeyName, jsonSession, found := strings.Cut(loginToken.Name, "---") + if !found { + return ErrInvalidPasskeyRegistrationData + } + err = json.Unmarshal([]byte(jsonSession), &pkeySession) + if err != nil { + return other.Error("auth", "failed to unmarshal passkey session for user", err) + } + wrappedAcc := fakeUser{&loginToken.User} + credential, err := a.webauthn.FinishRegistration(&wrappedAcc, pkeySession, response) + if err != nil { + return other.Error("auth", "failed to complete passkey registration", err) + } + jsonCredential, err := json.Marshal(credential) + if err != nil { + return other.Error("auth", "failed to marshal credential to json", err) + } + authData := models.UserAuthMethod{ + User: loginToken.User, + UserId: loginToken.UserId, + AuthMethod: models.AuthMethodPasskey, + Name: passkeyName, + Token: jsonCredential, + } + err = dbgen.UserAuthMethod.Create(&authData) + if err != nil { + return other.Error("auth", "failed to insert new auth method into db", err) + } + return nil } diff --git a/storage-new/models/0allTypes.go b/storage-new/models/0allTypes.go index d4b3f7d..05f1433 100644 --- a/storage-new/models/0allTypes.go +++ b/storage-new/models/0allTypes.go @@ -1,11 +1,14 @@ package models +// Just a list of all models stored in the database var AllTypes = []any{ &Emote{}, + &Feed{}, &MediaMetadata{}, &Note{}, &NoteToAttachment{}, &NoteToEmote{}, + &NoteToFeed{}, &NoteToPing{}, &NoteTag{}, &Reaction{}, diff --git a/storage-new/models/Feed.go b/storage-new/models/Feed.go new file mode 100644 index 0000000..9ad9d1c --- /dev/null +++ b/storage-new/models/Feed.go @@ -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 +} diff --git a/storage-new/models/FeedNoteToUser.go b/storage-new/models/FeedNoteToUser.go deleted file mode 100644 index 336a6fc..0000000 --- a/storage-new/models/FeedNoteToUser.go +++ /dev/null @@ -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 diff --git a/storage-new/models/NoteToFeed.go b/storage-new/models/NoteToFeed.go new file mode 100644 index 0000000..7f49b38 --- /dev/null +++ b/storage-new/models/NoteToFeed.go @@ -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 +} diff --git a/storage-new/models/User.go b/storage-new/models/User.go index 83c9cb0..9da82a2 100644 --- a/storage-new/models/User.go +++ b/storage-new/models/User.go @@ -10,15 +10,8 @@ import ( // A user describes an account for creating content. // This may be controlled by either a human or some external script // -// Data stored in external types: -// - Custom info fields -// - Being types -// - Tags -// - Relations -// - Pronouns -// - Roles -// - AP remote links -// - Auth methods and tokens (hashed pw, totp key, passkey id) +// Data stored externally: +// - Feed connections (which note belongs in the feed of this user, for what reason) type User struct { // ID is a uuid for this account //