feat: better but not perfect misskey auth support

This commit is contained in:
Sam 2023-03-25 15:54:09 +01:00
parent d9aa6e4fae
commit 75407827bc
No known key found for this signature in database
GPG key ID: B4EF20DDE721CAA1
6 changed files with 92 additions and 35 deletions

View file

@ -51,6 +51,10 @@ func (f FediverseApp) MastodonCompatible() bool {
return f.InstanceType == "mastodon" || f.InstanceType == "pleroma" || f.InstanceType == "akkoma" || f.InstanceType == "pixelfed" return f.InstanceType == "mastodon" || f.InstanceType == "pleroma" || f.InstanceType == "akkoma" || f.InstanceType == "pixelfed"
} }
func (f FediverseApp) Misskey() bool {
return f.InstanceType == "misskey" || f.InstanceType == "foundkey" || f.InstanceType == "calckey"
}
const ErrNoInstanceApp = errors.Sentinel("instance doesn't have an app") const ErrNoInstanceApp = errors.Sentinel("instance doesn't have an app")
func (db *DB) FediverseApp(ctx context.Context, instance string) (fa FediverseApp, err error) { func (db *DB) FediverseApp(ctx context.Context, instance string) (fa FediverseApp, err error) {

View file

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
@ -29,15 +30,6 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
return server.APIError{Code: server.ErrBadRequest} return server.APIError{Code: server.ErrBadRequest}
} }
// if the state can't be validated, return
if valid, err := s.validateCSRFState(ctx, decoded.State); !valid {
if err != nil {
return err
}
return server.APIError{Code: server.ErrInvalidState}
}
app, err := s.DB.FediverseApp(ctx, decoded.Instance) app, err := s.DB.FediverseApp(ctx, decoded.Instance)
if err != nil { if err != nil {
log.Errorf("getting app for instance %q: %v", decoded.Instance, err) log.Errorf("getting app for instance %q: %v", decoded.Instance, err)
@ -48,21 +40,25 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
} }
} }
token, err := app.ClientConfig().Exchange(ctx, decoded.Code) userkeyReq := struct {
if err != nil { AppSecret string `json:"appSecret"`
log.Errorf("exchanging oauth code: %v", err) Token string `json:"token"`
}{AppSecret: app.ClientSecret, Token: decoded.Code}
return server.APIError{Code: server.ErrInvalidOAuthCode} b, err := json.Marshal(userkeyReq)
if err != nil {
return errors.Wrap(err, "marshaling json")
} }
fmt.Println(string(b))
// make me user request // make me user request
req, err := http.NewRequestWithContext(ctx, "POST", "https://"+decoded.Instance+"/api/i", nil) req, err := http.NewRequestWithContext(ctx, "POST", "https://"+decoded.Instance+"/api/auth/session/userkey", bytes.NewReader(b))
if err != nil { if err != nil {
return errors.Wrap(err, "creating i request") return errors.Wrap(err, "creating userkey request")
} }
req.Header.Set("User-Agent", "pronouns.cc/"+server.Tag) req.Header.Set("User-Agent", "pronouns.cc/"+server.Tag)
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", token.Type()+" "+token.AccessToken)
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
@ -75,13 +71,20 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
return errors.Wrap(err, "reading i response") return errors.Wrap(err, "reading i response")
} }
var mu partialMisskeyAccount if resp.StatusCode < 200 || resp.StatusCode >= 400 {
err = json.Unmarshal(jb, &mu) log.Errorf("POST userkey for instance %q (type %v): %v", app.Instance, app.InstanceType, string(jb))
if err != nil { return errors.Wrap(err, "error on misskey's end")
return errors.Wrap(err, "unmarshaling i response")
} }
u, err := s.DB.FediverseUser(ctx, mu.ID, app.ID) var mu struct {
User partialMisskeyAccount `json:"user"`
}
err = json.Unmarshal(jb, &mu)
if err != nil {
return errors.Wrap(err, "unmarshaling userkey response")
}
u, err := s.DB.FediverseUser(ctx, mu.User.ID, app.ID)
if err == nil { if err == nil {
if u.DeletedAt != nil { if u.DeletedAt != nil {
// store cancel delete token // store cancel delete token
@ -104,7 +107,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
err = u.UpdateFromFedi(ctx, s.DB, mu.ID, mu.Username, app.ID) err = u.UpdateFromFedi(ctx, s.DB, mu.User.ID, mu.User.Username, app.ID)
if err != nil { if err != nil {
log.Errorf("updating user %v with misskey info: %v", u.ID, err) log.Errorf("updating user %v with misskey info: %v", u.ID, err)
} }
@ -141,7 +144,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
// no user found, so save a ticket + save their Misskey info in Redis // no user found, so save a ticket + save their Misskey info in Redis
ticket := RandBase64(32) ticket := RandBase64(32)
err = s.DB.SetJSON(ctx, "misskey:"+ticket, mu, "EX", "600") err = s.DB.SetJSON(ctx, "misskey:"+ticket, mu.User, "EX", "600")
if err != nil { if err != nil {
log.Errorf("setting misskey user for ticket %q: %v", ticket, err) log.Errorf("setting misskey user for ticket %q: %v", ticket, err)
return err return err
@ -149,7 +152,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
render.JSON(w, r, fediCallbackResponse{ render.JSON(w, r, fediCallbackResponse{
HasAccount: false, HasAccount: false,
Fediverse: mu.Username, Fediverse: mu.User.Username,
Ticket: ticket, Ticket: ticket,
RequireInvite: s.RequireInvite, RequireInvite: s.RequireInvite,
}) })
@ -360,13 +363,14 @@ func (s *Server) noAppMisskeyURL(ctx context.Context, w http.ResponseWriter, r *
return errors.Wrap(err, "creating app") return errors.Wrap(err, "creating app")
} }
state, err := s.setCSRFState(r.Context()) _, url, err := s.misskeyURL(ctx, app)
if err != nil { if err != nil {
return errors.Wrap(err, "setting CSRF state") log.Errorf("generating URL for misskey %q: %v", instance, err)
return errors.Wrap(err, "generating URL")
} }
render.JSON(w, r, FediResponse{ render.JSON(w, r, FediResponse{
URL: app.ClientConfig().AuthCodeURL(state), URL: url,
}) })
return nil return nil
} }
@ -382,3 +386,47 @@ type misskeyApp struct {
ID string `json:"id"` ID string `json:"id"`
Secret string `json:"secret"` Secret string `json:"secret"`
} }
func (s *Server) misskeyURL(ctx context.Context, app db.FediverseApp) (token, url string, err error) {
genSession := struct {
AppSecret string `json:"appSecret"`
}{AppSecret: app.ClientSecret}
b, err := json.Marshal(genSession)
if err != nil {
return token, url, errors.Wrap(err, "marshaling json")
}
req, err := http.NewRequestWithContext(ctx, "POST", "https://"+app.Instance+"/api/auth/session/generate", bytes.NewReader(b))
if err != nil {
log.Errorf("creating POST session request for %q: %v", app.Instance, err)
return token, url, errors.Wrap(err, "creating POST apps request")
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "pronouns.cc/"+server.Tag)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Errorf("sending POST session request for %q: %v", app.Instance, err)
return token, url, errors.Wrap(err, "sending POST apps request")
}
defer resp.Body.Close()
jb, err := io.ReadAll(resp.Body)
if err != nil {
log.Errorf("reading response for request: %v", err)
return token, url, errors.Wrap(err, "reading response")
}
var genSessionResp struct {
Token string `json:"token"`
URL string `json:"url"`
}
err = json.Unmarshal(jb, &genSessionResp)
if err != nil {
return token, url, errors.Wrap(err, "unmarshaling misskey response")
}
return genSessionResp.Token, genSessionResp.URL, nil
}

View file

@ -30,6 +30,18 @@ func (s *Server) getFediverseURL(w http.ResponseWriter, r *http.Request) error {
return s.noAppFediverseURL(ctx, w, r, instance) return s.noAppFediverseURL(ctx, w, r, instance)
} }
if app.Misskey() {
_, url, err := s.misskeyURL(ctx, app)
if err != nil {
return errors.Wrap(err, "generating misskey URL")
}
render.JSON(w, r, FediResponse{
URL: url,
})
return nil
}
state, err := s.setCSRFState(r.Context()) state, err := s.setCSRFState(r.Context())
if err != nil { if err != nil {
return errors.Wrap(err, "setting CSRF state") return errors.Wrap(err, "setting CSRF state")

View file

@ -69,9 +69,6 @@
</ListGroup> </ListGroup>
<Modal header="Pick an instance" isOpen={modalOpen} toggle={toggleModal}> <Modal header="Pick an instance" isOpen={modalOpen} toggle={toggleModal}>
<ModalBody> <ModalBody>
<p>
<strong>Note:</strong> Misskey (and derivatives) are not supported yet, sorry.
</p>
<Input placeholder="Instance (e.g. mastodon.social)" bind:value={instance} /> <Input placeholder="Instance (e.g. mastodon.social)" bind:value={instance} />
{#if error} {#if error}
<div class="mt-2"> <div class="mt-2">

View file

@ -8,8 +8,7 @@ export const load = (async ({ url, params }) => {
method: "POST", method: "POST",
body: { body: {
instance: params.instance, instance: params.instance,
code: url.searchParams.get("code"), code: url.searchParams.get("token"),
state: url.searchParams.get("state"),
}, },
}); });

View file

@ -128,9 +128,6 @@
</div> </div>
<Modal header="Pick an instance" isOpen={fediLinkModalOpen} toggle={toggleFediLinkModal}> <Modal header="Pick an instance" isOpen={fediLinkModalOpen} toggle={toggleFediLinkModal}>
<ModalBody> <ModalBody>
<p>
<strong>Note:</strong> Misskey (and derivatives) are not supported yet, sorry.
</p>
<Input placeholder="Instance (e.g. mastodon.social)" bind:value={instance} /> <Input placeholder="Instance (e.g. mastodon.social)" bind:value={instance} />
{#if error} {#if error}
<div class="mt-2"> <div class="mt-2">