Fediverse-based authentication (#1846)
* Able to authenticate user against IndieAuth. For #1273 * WIP server indieauth endpoint. For https://github.com/owncast/owncast/issues/1272 * Add migration to remove access tokens from user * Add authenticated bool to user for display purposes * Add indieauth modal and auth flair to display names. For #1273 * Validate URLs and display errors * Renames, cleanups * Handle relative auth endpoint paths. Add error handling for missing redirects. * Disallow using display names in use by registered users. Closes #1810 * Verify code verifier via code challenge on callback * Use relative path to authorization_endpoint * Post-rebase fixes * Use a timestamp instead of a bool for authenticated * Propertly handle and display error in modal * Use auth'ed timestamp to derive authenticated flag to display in chat * Fediverse chat auth via OTP * Increase validity time just in case * Add fediverse auth into auth modal * Text, validation, cleanup updates for fedi auth * Fix typo * Remove unused images * Remove unused file * Add chat display name to auth modal text
This commit is contained in:
@@ -40,6 +40,11 @@ func SendPublicFederatedMessage(message string) error {
|
||||
return outbox.SendPublicMessage(message)
|
||||
}
|
||||
|
||||
// SendDirectFederatedMessage will send a direct message to a single account.
|
||||
func SendDirectFederatedMessage(message, account string) error {
|
||||
return outbox.SendDirectMessageToAccount(message, account)
|
||||
}
|
||||
|
||||
// GetFollowerCount will return the local tracked follower count.
|
||||
func GetFollowerCount() (int64, error) {
|
||||
return persistence.GetFollowerCount()
|
||||
|
||||
@@ -17,13 +17,76 @@ const (
|
||||
PUBLIC PrivacyAudience = "https://www.w3.org/ns/activitystreams#Public"
|
||||
)
|
||||
|
||||
// MakeCreateActivity will return a new Create activity with the provided ID.
|
||||
func MakeCreateActivity(activityID *url.URL) vocab.ActivityStreamsCreate {
|
||||
activity := streams.NewActivityStreamsCreate()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(activityID)
|
||||
activity.SetJSONLDId(id)
|
||||
// MakeNotePublic ses the required proeprties to make this note seen as public.
|
||||
func MakeNotePublic(note vocab.ActivityStreamsNote) vocab.ActivityStreamsNote {
|
||||
public, _ := url.Parse(PUBLIC)
|
||||
to := streams.NewActivityStreamsToProperty()
|
||||
to.AppendIRI(public)
|
||||
note.SetActivityStreamsTo(to)
|
||||
|
||||
audience := streams.NewActivityStreamsAudienceProperty()
|
||||
audience.AppendIRI(public)
|
||||
note.SetActivityStreamsAudience(audience)
|
||||
|
||||
return note
|
||||
}
|
||||
|
||||
// MakeNoteDirect sets the required properties to make this note seen as a
|
||||
// direct message.
|
||||
func MakeNoteDirect(note vocab.ActivityStreamsNote, toIRI *url.URL) vocab.ActivityStreamsNote {
|
||||
to := streams.NewActivityStreamsCcProperty()
|
||||
to.AppendIRI(toIRI)
|
||||
to.AppendIRI(toIRI)
|
||||
note.SetActivityStreamsCc(to)
|
||||
|
||||
// Mastodon requires a tag with a type of "mention" and href of the account
|
||||
// for a message to be a "Direct Message".
|
||||
tagProperty := streams.NewActivityStreamsTagProperty()
|
||||
tag := streams.NewTootHashtag()
|
||||
tagTypeProperty := streams.NewJSONLDTypeProperty()
|
||||
tagTypeProperty.AppendXMLSchemaString("Mention")
|
||||
tag.SetJSONLDType(tagTypeProperty)
|
||||
|
||||
tagHrefProperty := streams.NewActivityStreamsHrefProperty()
|
||||
tagHrefProperty.Set(toIRI)
|
||||
tag.SetActivityStreamsHref(tagHrefProperty)
|
||||
tagProperty.AppendTootHashtag(tag)
|
||||
tagProperty.AppendTootHashtag(tag)
|
||||
note.SetActivityStreamsTag(tagProperty)
|
||||
|
||||
return note
|
||||
}
|
||||
|
||||
// MakeActivityDirect sets the required properties to make this activity seen
|
||||
// as a direct message.
|
||||
func MakeActivityDirect(activity vocab.ActivityStreamsCreate, toIRI *url.URL) vocab.ActivityStreamsCreate {
|
||||
to := streams.NewActivityStreamsCcProperty()
|
||||
to.AppendIRI(toIRI)
|
||||
to.AppendIRI(toIRI)
|
||||
activity.SetActivityStreamsCc(to)
|
||||
|
||||
// Mastodon requires a tag with a type of "mention" and href of the account
|
||||
// for a message to be a "Direct Message".
|
||||
tagProperty := streams.NewActivityStreamsTagProperty()
|
||||
tag := streams.NewTootHashtag()
|
||||
tagTypeProperty := streams.NewJSONLDTypeProperty()
|
||||
tagTypeProperty.AppendXMLSchemaString("Mention")
|
||||
tag.SetJSONLDType(tagTypeProperty)
|
||||
|
||||
tagHrefProperty := streams.NewActivityStreamsHrefProperty()
|
||||
tagHrefProperty.Set(toIRI)
|
||||
tag.SetActivityStreamsHref(tagHrefProperty)
|
||||
tagProperty.AppendTootHashtag(tag)
|
||||
tagProperty.AppendTootHashtag(tag)
|
||||
|
||||
activity.SetActivityStreamsTag(tagProperty)
|
||||
|
||||
return activity
|
||||
}
|
||||
|
||||
// MakeActivityPublic sets the required properties to make this activity
|
||||
// seen as public.
|
||||
func MakeActivityPublic(activity vocab.ActivityStreamsCreate) vocab.ActivityStreamsCreate {
|
||||
// TO the public if we're not treating ActivityPub as "private".
|
||||
if !data.GetFederationIsPrivate() {
|
||||
public, _ := url.Parse(PUBLIC)
|
||||
@@ -40,6 +103,16 @@ func MakeCreateActivity(activityID *url.URL) vocab.ActivityStreamsCreate {
|
||||
return activity
|
||||
}
|
||||
|
||||
// MakeCreateActivity will return a new Create activity with the provided ID.
|
||||
func MakeCreateActivity(activityID *url.URL) vocab.ActivityStreamsCreate {
|
||||
activity := streams.NewActivityStreamsCreate()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(activityID)
|
||||
activity.SetJSONLDId(id)
|
||||
|
||||
return activity
|
||||
}
|
||||
|
||||
// MakeUpdateActivity will return a new Update activity with the provided aID.
|
||||
func MakeUpdateActivity(activityID *url.URL) vocab.ActivityStreamsUpdate {
|
||||
activity := streams.NewActivityStreamsUpdate()
|
||||
@@ -61,9 +134,11 @@ func MakeUpdateActivity(activityID *url.URL) vocab.ActivityStreamsUpdate {
|
||||
// MakeNote will return a new Note object.
|
||||
func MakeNote(text string, noteIRI *url.URL, attributedToIRI *url.URL) vocab.ActivityStreamsNote {
|
||||
note := streams.NewActivityStreamsNote()
|
||||
|
||||
content := streams.NewActivityStreamsContentProperty()
|
||||
content.AppendXMLSchemaString(text)
|
||||
note.SetActivityStreamsContent(content)
|
||||
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(noteIRI)
|
||||
note.SetJSONLDId(id)
|
||||
@@ -77,17 +152,5 @@ func MakeNote(text string, noteIRI *url.URL, attributedToIRI *url.URL) vocab.Act
|
||||
attr.AppendIRI(attributedTo)
|
||||
note.SetActivityStreamsAttributedTo(attr)
|
||||
|
||||
// To the public if we're not treating ActivityPub as "private".
|
||||
if !data.GetFederationIsPrivate() {
|
||||
public, _ := url.Parse(PUBLIC)
|
||||
to := streams.NewActivityStreamsToProperty()
|
||||
to.AppendIRI(public)
|
||||
note.SetActivityStreamsTo(to)
|
||||
|
||||
audience := streams.NewActivityStreamsAudienceProperty()
|
||||
audience.AppendIRI(public)
|
||||
note.SetActivityStreamsAudience(audience)
|
||||
}
|
||||
|
||||
return note
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ type WebfingerResponse struct {
|
||||
Links []Link `json:"links"`
|
||||
}
|
||||
|
||||
// WebfingerProfileRequestResponse represents a Webfinger profile request response.
|
||||
type WebfingerProfileRequestResponse struct {
|
||||
Self string
|
||||
}
|
||||
|
||||
// Link represents a Webfinger response Link entity.
|
||||
type Link struct {
|
||||
Rel string `json:"rel"`
|
||||
@@ -41,3 +46,18 @@ func MakeWebfingerResponse(account string, inbox string, host string) WebfingerR
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MakeWebFingerRequestResponseFromData converts WebFinger data to an easier
|
||||
// to use model.
|
||||
func MakeWebFingerRequestResponseFromData(data []map[string]interface{}) WebfingerProfileRequestResponse {
|
||||
response := WebfingerProfileRequestResponse{}
|
||||
for _, link := range data {
|
||||
if link["rel"] == "self" {
|
||||
return WebfingerProfileRequestResponse{
|
||||
Self: link["href"].(string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package outbox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
@@ -13,7 +12,11 @@ import (
|
||||
"github.com/owncast/owncast/activitypub/apmodels"
|
||||
"github.com/owncast/owncast/activitypub/crypto"
|
||||
"github.com/owncast/owncast/activitypub/persistence"
|
||||
"github.com/owncast/owncast/activitypub/requests"
|
||||
"github.com/owncast/owncast/activitypub/resolvers"
|
||||
"github.com/owncast/owncast/activitypub/webfinger"
|
||||
"github.com/owncast/owncast/activitypub/workerpool"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
@@ -61,6 +64,12 @@ func SendLive() error {
|
||||
|
||||
activity, _, note, noteID := createBaseOutboundMessage(textContent)
|
||||
|
||||
// To the public if we're not treating ActivityPub as "private".
|
||||
if !data.GetFederationIsPrivate() {
|
||||
note = apmodels.MakeNotePublic(note)
|
||||
activity = apmodels.MakeActivityPublic(activity)
|
||||
}
|
||||
|
||||
note.SetActivityStreamsTag(tagProp)
|
||||
|
||||
// Attach an image along with the Federated message.
|
||||
@@ -106,6 +115,37 @@ func SendLive() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendDirectMessageToAccount will send a direct message to a single account.
|
||||
func SendDirectMessageToAccount(textContent, account string) error {
|
||||
links, err := webfinger.GetWebfingerLinks(account)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to get webfinger links when sending private message")
|
||||
}
|
||||
user := apmodels.MakeWebFingerRequestResponseFromData(links)
|
||||
|
||||
iri := user.Self
|
||||
actor, err := resolvers.GetResolvedActorFromIRI(iri)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to resolve actor to send message to")
|
||||
}
|
||||
|
||||
activity, _, note, _ := createBaseOutboundMessage(textContent)
|
||||
|
||||
// Set direct message visibility
|
||||
activity = apmodels.MakeActivityDirect(activity, actor.ActorIri)
|
||||
note = apmodels.MakeNoteDirect(note, actor.ActorIri)
|
||||
object := activity.GetActivityStreamsObject()
|
||||
object.SetActivityStreamsNote(0, note)
|
||||
|
||||
b, err := apmodels.Serialize(activity)
|
||||
if err != nil {
|
||||
log.Errorln("unable to serialize custom fediverse message activity", err)
|
||||
return errors.Wrap(err, "unable to serialize custom fediverse message activity")
|
||||
}
|
||||
|
||||
return SendToUser(actor.Inbox, b)
|
||||
}
|
||||
|
||||
// SendPublicMessage will send a public message to all followers.
|
||||
func SendPublicMessage(textContent string) error {
|
||||
originalContent := textContent
|
||||
@@ -191,6 +231,20 @@ func SendToFollowers(payload []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendToUser will send a payload to a single specific inbox.
|
||||
func SendToUser(inbox *url.URL, payload []byte) error {
|
||||
localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
|
||||
|
||||
req, err := requests.CreateSignedRequest(payload, inbox, localActor)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create outbox request")
|
||||
}
|
||||
|
||||
workerpool.AddToOutboundQueue(req)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateFollowersWithAccountUpdates will send an update to all followers alerting of a profile update.
|
||||
func UpdateFollowersWithAccountUpdates() error {
|
||||
// Don't do anything if federation is disabled.
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"github.com/owncast/owncast/activitypub/crypto"
|
||||
"github.com/owncast/owncast/config"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -50,3 +54,21 @@ func WriteResponse(payload []byte, w http.ResponseWriter, publicKey crypto.Publi
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateSignedRequest will create a signed POST request of a payload to the provided destination.
|
||||
func CreateSignedRequest(payload []byte, url *url.URL, fromActorIRI *url.URL) (*http.Request, error) {
|
||||
log.Debugln("Sending", string(payload), "to", url)
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPost, url.String(), bytes.NewBuffer(payload))
|
||||
|
||||
ua := fmt.Sprintf("%s; https://owncast.online", config.GetReleaseString())
|
||||
req.Header.Set("User-Agent", ua)
|
||||
req.Header.Set("Content-Type", "application/activity+json")
|
||||
|
||||
if err := crypto.SignRequest(req, payload, fromActorIRI); err != nil {
|
||||
log.Errorln("error signing request:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
46
activitypub/webfinger/webfinger.go
Normal file
46
activitypub/webfinger/webfinger.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package webfinger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetWebfingerLinks will return webfinger data for an account.
|
||||
func GetWebfingerLinks(account string) ([]map[string]interface{}, error) {
|
||||
type webfingerResponse struct {
|
||||
Links []map[string]interface{} `json:"links"`
|
||||
}
|
||||
|
||||
account = strings.TrimLeft(account, "@") // remove any leading @
|
||||
accountComponents := strings.Split(account, "@")
|
||||
fediverseServer := accountComponents[1]
|
||||
|
||||
// HTTPS is required.
|
||||
requestURL, err := url.Parse("https://" + fediverseServer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse fediverse server host %s", fediverseServer)
|
||||
}
|
||||
|
||||
requestURL.Path = "/.well-known/webfinger"
|
||||
query := requestURL.Query()
|
||||
query.Add("resource", fmt.Sprintf("acct:%s", account))
|
||||
requestURL.RawQuery = query.Encode()
|
||||
|
||||
response, err := http.DefaultClient.Get(requestURL.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
var links webfingerResponse
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&links); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return links.Links, nil
|
||||
}
|
||||
Reference in New Issue
Block a user