forked from mirrors/pronouns.cc
feat: better but not perfect misskey auth support
This commit is contained in:
parent
d9aa6e4fae
commit
75407827bc
6 changed files with 92 additions and 35 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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"),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in a new issue