1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package atproto
import (
"context"
"errors"
"fmt"
"strings"
"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/xrpc"
"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/vault"
"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/conds"
"github.com/johnandersen777/socialweb-computer-kcp/internal/pds/identity"
)
type SessionsFunc func(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity) (vault.AtprotoSessions, error)
var ErrSignInAgain = errors.New("marketidentity: the owner must sign in to the web app again")
func (a *Accounts) sessionProxy(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity, generation int64,
status v1alpha1.MarketIdentityStatus) (Result, error) {
did := strings.TrimSpace(id.Spec.Did)
ref := id.Spec.SessionRef
if did == "" || ref == nil || ref.Vault == nil {
return failed(status, generation, "InvalidSpec",
"mode oauthSessionProxy needs spec.did and a spec.sessionRef naming the vault, the atproto auth mount and session/<did>"), nil
}
session, err := a.proxiedSession(ctx, workspace, id)
switch {
case errors.Is(err, vault.ErrNoAtprotoSession):
return a.signInAgain(status, generation, id, "NoSession",
fmt.Sprintf("the web app holds no session for %s in %s; the owner must sign in to the web app again", did, ref.Mount)), ErrSignInAgain
case errors.Is(err, vault.ErrAtprotoSessionInactive):
return a.signInAgain(status, generation, id, "SessionInactive",
fmt.Sprintf("the web app session of %s is not active (%v); the owner must sign in to the web app again", did, err)), ErrSignInAgain
case err != nil:
return pending(status, generation, "SessionProxyUnavailable", "reading the web app session of "+did+" through the vault: "+err.Error()), err
case session.DID != did:
return failed(status, generation, "SubjectMismatch", fmt.Sprintf("the web app session at %s/%s is for %s, not %s", ref.Mount, ref.Path, session.DID, did)), nil
case session.Status != vault.AtprotoSessionActive:
return a.signInAgain(status, generation, id, "SessionInactive",
fmt.Sprintf("the web app session of %s is %s (%s); the owner must sign in to the web app again", did, session.Status, session.StatusReason)), ErrSignInAgain
case strings.TrimSpace(session.PDS) == "":
return pending(status, generation, "SessionProxyUnavailable", "the web app session of "+did+" names no PDS"), nil
}
held := a.held.get(workspace, id)
held.open.Lock()
err = a.scopeCheck(workspace, id, held, strings.Fields(session.Scope))
held.open.Unlock()
if changed := (*scopeChangedError)(nil); errors.As(err, &changed) {
held.auth.Drop()
status.Did, status.PdsURL = did, strings.TrimRight(session.PDS, "/")
return a.signInAgain(status, generation, id, "ScopeChanged",
fmt.Sprintf("the web app session of %s lacks scopes the gateway web app now requests (%s); sign in to the web app again",
did, strings.Join(changed.subjects(), ", "))), ErrSignInAgain
}
status.Phase = v1alpha1.MarketIdentityPhaseReady
status.ObservedGeneration = generation
status.Did = did
status.Handle = firstNonEmpty(session.Handle, id.Spec.Handle)
status.PdsURL = strings.TrimRight(session.PDS, "/")
status.SessionRef = ref
status.Associations = nil
status.PendingAction = nil
status.Message = ""
conds.Set(&status.Conditions, generation, v1alpha1.ConditionSignedIn, true, "SignedIn",
"the web app session in "+ref.Mount+" is active; the atproto auth plugin refreshes it and signs each request")
setReady(&status, generation, true, "SignedIn", did)
return Result{Status: status}, nil
}
func (a *Accounts) signInAgain(status v1alpha1.MarketIdentityStatus, generation int64, id *v1alpha1.MarketIdentity, reason, message string) Result {
status.Phase = v1alpha1.MarketIdentityPhaseSessionExpired
status.ObservedGeneration = generation
status.SessionRef = id.Spec.SessionRef
status.PendingAction = &v1alpha1.MarketIdentityPendingAction{Kind: v1alpha1.PendingActionSignInAgain, Handle: id.Spec.Handle}
status.Message = message
conds.Set(&status.Conditions, generation, v1alpha1.ConditionSignedIn, false, reason, message)
setReady(&status, generation, false, reason, message)
return Result{Status: status}
}
func (a *Accounts) proxiedSession(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity) (vault.AtprotoSession, error) {
if a.sessions == nil {
return vault.AtprotoSession{}, errors.New("marketidentity: this core resolves no vault atproto session handle")
}
sessions, err := a.sessions(ctx, workspace, id)
if err != nil {
return vault.AtprotoSession{}, err
}
return sessions.Session(ctx, id.Spec.SessionRef.Mount, id.Spec.SessionRef.Path)
}
func (a *Accounts) holdProxy(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity, held *heldAccount) error {
ref := id.Spec.SessionRef
did, pdsURL := strings.TrimSpace(id.Status.Did), strings.TrimSpace(id.Status.PdsURL)
if ref == nil || ref.Vault == nil || did == "" || pdsURL == "" || id.Status.Phase != v1alpha1.MarketIdentityPhaseReady {
return fmt.Errorf("%w: the web app session of %s is not ready (%s)", ErrSignInAgain, firstNonEmpty(did, id.Spec.Did), id.Status.Message)
}
if a.sessions == nil {
return errors.New("marketidentity: this core resolves no vault atproto session handle")
}
held.open.Lock()
defer held.open.Unlock()
if p := held.auth.Proxied(); p != nil && held.ref == *ref && held.auth.DID() == did && p.PdsURL == pdsURL {
return nil
}
client, err := a.sessionHTTP(ctx, workspace, id)
if err != nil {
return err
}
mount, path := ref.Mount, ref.Path
snapshot := id.DeepCopy()
prover := xrpc.DPoPProverFunc(func(ctx context.Context, method, target, _, nonce string) (xrpc.DPoPProof, error) {
sessions, err := a.sessions(ctx, workspace, snapshot)
if err != nil {
return xrpc.DPoPProof{}, err
}
proof, err := sessions.Proof(ctx, mount, path, method, target, nonce)
if err != nil {
return xrpc.DPoPProof{}, err
}
return xrpc.DPoPProof{AccessToken: proof.AccessToken, Proof: proof.DPoP}, nil
})
held.ref = *ref
held.auth.HoldProxy(did, &identity.Proxy{PdsURL: pdsURL, HTTP: client, Prover: prover})
return nil
}